Java9-秘籍-全-

Java9 秘籍(全)

原文:Java 9 Recipes

协议:CC BY-NC-SA 4.0

一、Java 9 入门

在这一章中,我们提供了一些方法来帮助刚接触 Java 语言的程序员以及那些有其他语言经验的程序员熟悉 Java 9。您将学习安装 Java,还将安装一个集成开发环境(IDE ),从中您将开发应用并试验本书中提供的解决方案。你将学习 Java 的基础知识,比如如何创建一个类,以及如何接受键盘输入。文档经常被忽视,但是在这一章中,你将很快学会如何为你的 Java 代码创建优秀的文档。

注意

Java 9 食谱并不是一个完整的教程。相反,它涵盖了 Java 语言的关键概念。如果你真的是 Java 新手,我们推荐你购买和阅读由 Apress 出版的许多入门 Java 书籍中的一本。

1-1.创建开发环境

问题

您想安装 Java 并尝试使用这种语言。您还希望有一个合理的 IDE 来使用它。

解决办法

安装 Java 开发工具包 9 (JDK。这给了你语言和编译器。然后安装 NetBeans IDE,以提供更高效的工作环境。

Java 标准版(Java SE)对于这本书里的大部分食谱来说已经足够了。要下载该版本,请访问甲骨文技术网(OTN)的以下页面:

www . Oracle . com/tech network/Java/javase/overview/index . html

图 1-1 显示了下载选项卡,你可以在页面显著位置看到 Java 平台下载链接和图片。该链接旁边是 NetBeans IDE 的图像,它提供了将 JDK 和 NetBeans 一起下载的选项。选择您喜欢的选项,下载适用于您的平台的版本,然后运行安装向导进行安装。出于本书的目的,我使用 NetBeans IDE 8.2。

A323910_3_En_1_Fig1_HTML.jpg

图 1-1。OTN 上的 Java SE 下载页面
注意

如果您选择只安装 Java 平台(JDK)而不安装 NetBeans,您可以稍后访问 netbeans.org 下载 NetBeans。

它是如何工作的

名称Java是甲骨文公司的商标。语言本身是开源的,它的发展由一个叫做 Java 社区进程 (JCP)的进程控制。你可以在 www.jcp.org 的了解更多关于这个过程的信息。

虽然这种语言本身并不归甲骨文公司所有,但它的核心开发往往是由甲骨文公司主导的。甲骨文公司经营着 jcp.org 的 JCP,并拥有其域名。

Java 有很多版本,比如移动版(ME)和企业版(EE)。Java SE 是标准版,代表了语言的核心。我们在本书中为 Java SE 程序员构建了食谱。那些对为 Raspberry Pi 等设备开发嵌入式应用感兴趣的人可能有兴趣了解更多关于 Java ME 的知识。类似地,那些对开发 web 应用和使用企业解决方案感兴趣的人可能会有兴趣学习更多关于 Java EE 的知识。

注意

企业开发人员可能想要购买并阅读乔希·朱诺号(Apress,2013)的《Java EE 7 食谱》。

有几个不错的网站,您可以访问它们来了解更多关于 Java 的知识,并了解该平台的最新动态。OTN 上的以下页面是开始学习所有 Java 知识的好地方:

www.oracle.com/technetwork/java/index.html

该页面提供的丰富资源一开始可能会让人不知所措,但是值得您花时间四处看看,顺便熟悉一下许多可用的链接。

其中一个链接指向 Java SE,它会带你进入图 1-1 中所示的页面。您可以从那里下载 Java SE 和 NetBeans IDE。从那里,您还可以访问官方文档、论坛和时事通讯等社区资源,以及旨在帮助您积累 Java 知识并获得该语言认证的培训资源。

1-2.进入“你好,世界”

问题

您已经安装了 Java SE 9 和 NetBeans IDE。现在您想运行一个简单的 Java 程序来验证您的安装是否正常工作。

解决办法

首先打开 NetBeans IDE。您应该会看到一个类似于图 1-2 中的工作区。如果您已经在 IDE 中处理过项目,您可能会在左侧窗格中看到一些项目。

A323910_3_En_1_Fig2_HTML.jpg

图 1-2。打开 NetBeans IDE

转到文件菜单并选择新建项目。您将看到图 1-3 中的对话框。选择 Java 类别,然后选择 Java 应用。点击下一步进入如图 1-4 所示的对话框。

A323910_3_En_1_Fig3_HTML.jpg

图 1-3。创建新的 Java SE 项目

A323910_3_En_1_Fig4_HTML.jpg

图 1-4。命名项目

为您的项目命名。对于与本书相关的项目,使用名称 Java9Recipes。在图 1-4 对话框顶部的项目名称文本框中输入项目名称。

然后在“创建主类”文本框中指定主类的名称。给出以下名称:

org.java9recipes.chapter01.recipe1_02.HelloWorld

确保您已经输入了项目名和类名,就像我们在这里提供的一样,因为要遵循的代码取决于您这样做。确保“项目名称”文本框指定 Java9Recipes。确保“创建主类”文本框指定 org . Java 9 recipes . chapter 01 . recipe 1 _ 02 . hello world。

小费

注意大小写;Java 是区分大小写的。

按“完成”以完成向导并创建一个框架项目。您现在应该看到一个 Java 源文件。框架代码已经生成,您的 NetBeans IDE 窗口应该类似于图 1-5 中的窗口。

A323910_3_En_1_Fig5_HTML.jpg

图 1-5。查看 NetBeans 生成的框架代码

将光标放在源代码窗格中的任意位置。按 Ctrl-A 选择所有的骨架代码。然后按 Delete 键删除它。用清单 1-1 中的代码替换删除的代码。

您可以在清单 1-1 中找到代码,作为本书示例下载的一部分。有两个名为 HelloMessage.java 和 HelloWorld.java 的文件,它们位于名为 org . Java 9 recipes . chapter 01 . recipe 1 _ 02 的 Java 包中。请注意,本书中所有配方溶液都在下载的例子中。

第一个类 HelloMessage 是一个容器类,用于保存基于字符串的消息。

清单 1-1。一个“你好,世界”的例子
package org.java9recipes.chapter01.recipe1_02;

public class HelloMessage {
    private String message = "";
    public HelloMessage() {
        this.message = "Default Message";
    }
    public void setMessage (String m) {
        this.message = m;
    }
    public String getMessage () {
        return message.toUpperCase();
    }   
}

The next class is named HelloWorld, and it initiates the program:

public class HelloWorld {
    /* The main method begins in this class */

    public static void main(String[] args) {
        HelloMessage hm;     
        hm = new HelloMessage();
        System.out.println(hm.getMessage());
        hm.setMessage("Hello, World");
        System.out.println(hm.getMessage());    
    }
}

确保您已经粘贴(或键入)了清单 1-1 中的代码。编译并运行该程序,您应该会看到以下输出:

run:
DEFAULT MESSAGE
HELLO, WORLD
BUILD SUCCESSFUL (total time: 1 second)

此输出将出现在一个名为“输出”的新窗格中,该窗格由 NetBeans 在 IDE 窗口的底部打开。

它是如何工作的

您可以使用本菜谱中显示的通用技术运行本章中几乎所有的解决方案。出于这个原因,我们已经煞费苦心地详细说明了这一点,只展示了一步一步的截图。

包装

解决方案示例首先创建一个 Java :

package org.java9recipes.chapter01.recipe1_02;

包是将相关类组合到一个共享名称空间中的一种方式。这个想法是通过以相反的顺序沿着你的组织的域名前进来实现普遍的唯一性。习惯上也是用小写来写包名。

NetBeans 将创建一个目录结构来模拟您的包路径。在这种情况下,NetBeans 创建了以下目录路径:

C:\Users\JonathanGennick\Documents\NetBeansProjects\
Java9Recipes\src\org\java9recipes\chapter01\recipe1_02

以下是关于此路径的一些注意事项:

  • 前面部分是 C:\Users...\ netbeans 项目。除非您另外指定,否则 NetBeans 会在 NetbeansProject 目录下创建所有项目,您可以从图 1-4 中的对话框中进行指定。许多开发人员指定较短的路径。

  • 接下来是 Java9Recipes 的第一次出现。该事件对应于您在图 1-4 的项目名称文本框中填写的项目名称。

  • 您创建的任何源文件都进入 src 目录。NetBeans 在此级别创建其他目录。例如,NetBeans 创建一个构建目录,然后在其下是一个 classes 子目录来保存您编译的类文件。

  • 最后是镜像您指定的包路径的目录,在本例中是 org \ Java 9 recipes \ chapter 01 \ recipe 1 _ 02。编译代码时,在 build\classes 目录下会创建一个相同的结构。请注意,如果使用另一个 IDE,您可能会看到创建的目录有所不同。

您不需要显式创建包。如果您没有创建,Java 编译器会为您创建一个,并给它一个隐藏的名称。我们喜欢直截了当,你也应该这样。在专业环境中,考虑周全并明确 Java 包名是必要的。在开发任何重要的应用时,组织以及明智选择的命名约定都很重要。

JavaBeans 风格的类

接下来,在解决方案示例中,您将看到一个遵循 JavaBeans 模式的类定义。HelloMessage 的定义遵循了您在 Java 编程中经常遇到的模式,因此我们包含了它。这个类很简单,能够保存一个名为 message 的字符串字段。

该类定义了三种方法:

  • HelloMessage()。此方法也称为构造函数,与类同名。在这种情况下,它不需要任何参数。每当你创建一个新的类对象时,它都会被自动调用。请注意,这被称为“无参数”构造函数,因为它是在类中类型化的,并且不带任何参数。如果不提供构造函数,JVM 将自动提供一个默认的构造函数(也没有参数)。

  • setMessage(字符串)。这个访问器方法以单词 set 开始。它需要一个参数。它指定由相应的 get 方法返回的消息。

  • getMessage()。这个访问器方法返回当前定义的消息。在我们的例子中,我们选择大写消息。

注意

JavaBeans 类中使用访问器方法来访问任何私有声明的类成员。在这种情况下,可以使用这些方法访问标识为 message 的私有变量。访问器方法通常被称为“getters”和“setters”

以 set 和 get 开头的方法被称为 settergetter 方法。变量 message 是类私有的,这意味着您不能从类外部直接访问消息。

你会在课上看到关键词 this。它是 Java 中的一个特殊关键字,用来引用当前对象。它的使用在清单 1-1 中是多余的,但是如果任何一个方法碰巧创建了它们自己的变量,并且这些变量也被命名为 message,那么就需要使用它。通常的做法是使用“this”关键字从“getter”和“setter”方法中引用类成员。

在 Java 中,通过 setter 和 getter 方法来协调对类变量的访问是很常见的,就像我们示例中的方法一样。这些方法代表了与其他类和你的主程序的某种契约。它们的好处是您可以随意更改 HelloMessage 的存储实现。只要您保持 setMessage()和 getMessage()的外部行为,依赖于 HelloMessage 的其他代码将继续正常工作。

主程序

咒语公共静态空主(...)在公共类中用来表示 Java 程序的入口点。该声明开始了一个名为 main 的可执行方法。您必须指定一个字符串数组参数,通常该参数被定义为 String[] args。

当您执行当前选定的类时,NetBeans 会将代码编译成一组二进制文件,然后将控制权转移给 main()方法。NetBeans 也可以配置为在保存时重新编译,这将导致控制权转移到 main()方法。该方法依次执行以下操作:

  1. 执行 HelloMessage 以创建一个名为 hm 的变量,该变量能够保存 HelloMessage 类的实例。变量 hm 此时为空。

  2. 调用新的 HelloMessage()以该名称创建该类的对象。将执行无参数构造函数,现在“默认消息”被设置为问候文本。新对象现在存储在变量 hm 中。

  3. 调用 System.out.println()以显示对象的无参数构造函数确实已按预期执行。问候语“默认消息”显示在“输出”窗格中。

  4. 将消息设置为传统文本“Hello,World”。

  5. 再次调用 System.out.println()以输出刚刚设置的新消息。现在你可以看到“HELLO,WORLD”这个问候语被添加到了“输出”窗格中。

解决方案中的模式在 Java 编程中很常见。main()方法是执行开始的地方。使用 new 运算符定义变量,创建对象。通常使用 setter 和 getter 方法来设置和检索对象变量。

小费

命令行应用已经过时了。系统管理员和程序员有时将它们作为实用程序来编写,或者批量处理大量数据。但是总的来说,今天的大多数应用都是 GUI 应用。JavaFX 是编写标准桌面应用的前进方向,你可以在第 14 到 16 章中了解到它。Recipe 14-1 以 GUI 形式提供了本质上是“Hello,World”的应用。JavaEE 提供了为 Java 平台开发基于 web 的应用的选项,你可以在第十七章中了解更多。

1-3.配置类路径

问题

您想要执行一个 Java 程序,或者在您正在执行的应用中包含一个外部 Java 库。

解决办法

将 CLASSPATH 变量设置为用户定义的 Java 类或 Java 归档(JAR)文件的目录位置,您需要访问这些文件来执行应用。假设您在 OS 驱动器的根目录下有一个名为 JAVA_DEV 的目录,您的应用需要访问的所有文件都位于这个目录中。如果是这种情况,您应该执行如下命令:

set CLASSPATH=C:\JAVA_DEV\some-jar.jar

或者在 Unix 和 Linux 系统上:

export CLASSPATH=/JAVA_DEV/some-jar.jar

或者,javac 命令提供了一个选项,用于指定需要为应用加载的资源的位置。在所有平台上,使用这种技术设置类路径可以通过-classpath 选项来完成,如下所示:

javac –classpath /JAVA_DEV/some-jar.jar

当然,在 Microsoft Windows 机器上,文件路径将使用反斜杠()来代替。

注意

可以使用 javac–CP 选项,而不是指定–class path 选项。

它是如何工作的

Java 实现了类路径的概念。这是一个目录搜索路径,可以使用 CLASSPATH 环境变量在系统范围内指定。还可以通过 java 命令的-classpath 选项为 JVM 的特定调用指定类路径。(参见配方 1-4 中的示例。)

注意

对于未来的许多 Java 应用来说,类路径肯定仍然很重要。然而,Java 9 中引入的新模块系统取代了那些利用模块化构建的应用使用脆弱类路径的需要。参见第二十二章了解更多关于 Java 模块化的信息。

当执行 Java 程序时,JVM 使用以下搜索顺序根据需要查找和加载类:

  1. 这些类是 Java 平台的基础,包含在 Java 安装目录中。

  2. 位于 JDK 的扩展目录中的任何包或 JAR 文件。

  3. 在指定的类路径上加载的包、类、JAR 文件和库。

您可能需要为一个应用访问多个目录或 JAR 文件。如果您的依赖项位于多个位置,就会出现这种情况。为此,只需使用操作系统的分隔符(;或者:)作为 CLASSPATH 变量指定的位置之间的分隔符。以下是在 Unix 和 Linux 系统上的 CLASSPATH 环境变量中指定多个 JAR 文件的示例:

export CLASSPATH=/JAVA_DEV/some-jar.jar:/JAVA_LIB/myjar.jar

或者,您可以通过命令行选项指定类路径:

javac –classpath /JAVA_DEV/some-jar.jar:/JAVA_LIB/myjar.jar

当加载 Java 应用的资源时,JVM 加载第一个位置指定的所有类和包,然后是第二个位置,依此类推。这一点很重要,因为在某些情况下,加载的顺序可能会有所不同。

注意

JAR 文件用于将应用和 Java 库打包成可分发的格式。如果您没有以那种方式打包您的应用,您可以简单地指定您的。类文件驻留在。

有时,您会希望将所有 JAR 文件包含在一个指定的目录中。为此,请在包含文件的目录后指定通配符(*)。例如:

javac –classpath /JAVA_DEV/*:/JAVA_LIB/myjar.jar

指定通配符将告诉 JVM 它应该只加载 JAR 文件。它不会加载位于用通配符指定的目录中的类文件。如果还需要这些类文件,您需要为同一个目录指定一个单独的路径条目。例如:

javac –classpath /JAVA_DEV/*:/JAVA_DEV

不会搜索类路径中的子目录。为了加载子目录中包含的文件,这些子目录和/或文件必须在类路径中明确列出。但是,相当于子目录结构的 Java 包会被加载。因此,驻留在相当于子目录结构的 Java 包中的任何 Java 类都将被加载。

注意

组织您的代码是一个好主意;组织你在计算机上放置代码的位置也是很好的。一个好的做法是将所有的 Java 项目放在同一个目录中;它可以成为你的工作空间。将 JAR 文件中包含的所有 Java 库放在同一个目录中,以便于管理。

1-4.用包组织代码

问题

您的应用由一组 Java 类、接口和其他类型组成。您希望组织这些源文件,使它们更易于维护,并避免潜在的类命名冲突。

解决办法

创建 Java 包并将源文件放入其中,就像文件系统一样。Java 包可以用来组织应用中源文件的逻辑组。包可以帮助组织代码,减少不同类和其他 Java 类型文件之间的命名冲突,并提供访问控制。要创建一个包,只需在应用源文件夹的根目录下创建一个目录,并将其命名为。包通常相互嵌套,并且符合标准的命名约定。为了这个食谱的目的,假设该组织名为朱诺号,该组织制造小部件。要组织小部件应用的所有代码,请创建一组符合以下目录结构的嵌套包:

/org/juneau

放在包中的任何源文件都必须包含 package 语句作为源文件的第一行。package 语句列出包含源文件的包的名称。例如,假设小部件应用的主类名为 JuneauWidgets.java。要将这个类放入名为 org.juneau 的包中,请将源文件移动到名为 juneau 的目录中,该目录位于 org 目录中,而 org 目录又位于应用的源文件夹的根目录中。目录结构应该如下所示:

/org/juneau/JuneauWidgets.java

JuneauWidgets.java 的资料来源如下:

package org.juneau;

/**
 * The main class for the Juneau Widgets application.
 * @author juneau
 */
public class JuneauWidgets {
    public static void main(String[] args){
        System.out println("Welcome to my app!");
    }
}

源代码中的第一行包含 package 语句,该语句列出了源文件所在的包的名称。语句中列出了整个包路径,路径中的名称用点分隔。

注意

package 语句必须是 Java 源代码中列出的第一条语句。但是,在 package 语句之前可能会有一个注释或 Javadoc 注释。有关注释或 Javadoc 的更多信息,请参见配方 1-12。

一个应用可以由任意数量的包组成。如果小部件应用包含一些表示小部件对象的类,它们可以放在 org.juneau.widget 包中。该应用可以具有可用于与窗口小部件对象交互的接口。在这种情况下,可能还存在一个名为 org.juneau.interfaces 的包来包含任何这样的接口。

它是如何工作的

Java 包对于组织源文件、控制对不同类的访问以及确保没有命名冲突非常有用。包由文件系统上的一系列物理目录表示,它们可以包含任意数量的 Java 源文件。每个源文件必须在文件中的任何其他语句之前包含一个 package 语句。此 package 语句列出源文件所在的包的名称。在该配方的解决方案中,源代码包括以下程序包语句:

package org.juneau;

这个 package 语句表明源文件位于一个名为 juneau 的目录中,该目录位于另一个名为 org 的目录中。包命名约定可能因公司或组织而异。然而,重要的是单词全部小写,这样它们就不会与任何 Java 类文件名冲突。许多公司或组织将使用其域名的反义词来命名软件包。但是,如果域名包含连字符,则应使用下划线。

注意

当一个类驻留在 Java 包中时,它不再仅仅由类名引用,而是包名被添加到类名的前面,这就是所谓的完全限定的名。例如,因为驻留在文件 JuneauWidgets.java 中的类包含在 org.juneau 包中,所以使用 org.juneau.JuneauWidgets 而不仅仅是 JuneauWidgets 来引用该类。同名的类可以驻留在不同的包中(例如 org.java9recipes.JuneauWidgets)。

包对于建立安全级别和组织非常有用。默认情况下,位于同一个包中的不同类可以相互访问。如果源文件与它需要使用的另一个文件驻留在不同的包中,则必须在源文件的顶部(包语句下)声明 import 语句,以导入该另一个文件供使用;否则,必须在代码中使用完全限定的 package.class 名称。可以单独导入类,如下面的 import 语句所示:

import org.juneau.JuneauWidgets;

但是,通常可能需要使用包中的所有类和类型文件。使用通配符(*)的单个 import 语句可以导入命名包中的所有文件,如下所示:

import org.juneau.*;

虽然可以导入所有文件,但除非绝对必要,否则不建议这样做。事实上,包含许多使用通配符的 import 语句被认为是一种糟糕的编程实践。相反,类和类型文件应该单独导入。

在包中组织类可以证明是非常有用的。假设这个配方的解决方案中描述的小部件应用为每个不同的小部件对象包含不同的 Java 类。每个小部件类都可以分组到一个名为 org.juneau.widgets 的包中。所有这些接口都可以组织成一个名为 org.juneau.interfaces 的包。

任何实际的 Java 应用都会包含包。您使用的任何 Java 库或应用编程接口(API)都包含包。当您从那些库和 API 中导入类或类型时,您实际上是在导入包。

1-5.声明变量和访问修饰符

问题

你想在你的程序中创建一些变量和操作数据。此外,您希望某些变量只对当前的类可用,而其他的变量应该对所有的类可用,或者只对当前包中的其他类可用。

解决办法

Java 实现了八种基本数据类型。还有对字符串类类型的特殊支持。清单 1-2 显示了每个声明的示例。从示例中提取,以声明您自己的应用中所需的变量。

清单 1-2。基本类型和字符串类型的声明
package org.java9recipes.chapter01.recipe1_05;

public class DeclarationsExample {
    public static void main (String[] args) {
        boolean BooleanVal = true;  /* Default is false */
        char charval = 'G';     /* Unicode UTF-16 */
        charval = '\u0490';     /* Ukrainian letter Ghe(Ґ) */

        byte byteval;       /*  8 bits, -127 to 127 */
        short shortval;     /* 16 bits, -32,768 to 32,768 */
        int intval;         /* 32 bits, -2147483648 to 2147483647 */
        long longval;       /* 64 bits, -(2⁶⁴) to 2⁶⁴ - 1 */

        float   floatval = 10.123456F; /* 32-bit IEEE 754 */
        double doubleval = 10.12345678987654; /* 64-bit IEEE 754 */

        String message = "Darken the corner where you are!";
        message = message.replace("Darken", "Brighten");
    }
}
注意

如果你对清单 1-2 中的乌克兰字母感到好奇,那就是带上翻的西里尔字母Ghe。你可以在 http://en.wikipedia.org/wiki/Ghe_with_upturn 了解它的历史。你可以在图表中的www.unicode.org/charts/PDF/U0400.pdf找到它的码位值。当您需要查找与给定字符对应的代码点时,URLwww.unicode.org/charts/是一个很好的起点。

变量受制于可见性的概念。在清单 1-2 中创建的那些函数在创建后可以从 main()方法中看到,并且在 main()方法结束时被释放。它们在 main()方法之外没有“生命”,并且不能从 main()之外访问。

在类级别创建的变量是一个不同的故事。这样的变量可以被称为类字段或类成员,就像在字段或类成员中一样。成员的使用可以限制在声明它的类的对象、声明它的包中,或者可以从任何包中的任何类访问它。清单 1-3 展示了如何通过私有和公共关键字来控制可见性。

清单 1-3。可见度和场的概念
package org.java9recipes.chapter01.recipe1_05;

class TestClass {
    private long visibleOnlyInThisClass;
    double visibleFromEntirePackage;
    void setLong (long val) {
        visibleOnlyInThisClass = val;
    }
    long getLong () {
       return visibleOnlyInThisClass;
    }  
}

public class VisibilityExample {            
    public static void main(String[] args) {
        TestClass tc = new TestClass();  
        tc.setLong(32768);
        tc.visibleFromEntirePackage = 3.1415926535;
        System.out.println(tc.getLong());
        System.out.println(tc.visibleFromEntirePackage);
    }
}

输出:

32768
3.1415926535

成员通常绑定到一个类的对象。类的每个对象都包含该类中每个成员的一个实例。然而,您也可以定义只出现一次的所谓的静态字段,并且给定类的所有实例共享一个值。清单 1-4 说明了区别。

清单 1-4。静态字段
package org.java9recipes.chapter01.recipe1_05;

class StaticDemo {
    public static boolean oneValueForAllObjects = false;
}

public class StaticFieldsExample {
    public static void main (String[] args) {
        StaticDemo sd1 = new StaticDemo();
        StaticDemo sd2 = new StaticDemo();
        System.out.println(sd1.oneValueForAllObjects);
        System.out.println(sd2.oneValueForAllObjects);
        sd1.oneValueForAllObjects = true;
        System.out.println(sd1.oneValueForAllObjects);
        System.out.println(sd2.oneValueForAllObjects);
    }
}

清单 1-4 产生以下输出:

false
false
true
true

只有名为 sd1 的类实例的 oneValueForAllObjects 字段设置为 true。然而对于 sd2 来说也是如此。这是因为在声明该字段时使用了关键字 static。静态字段对其类中的所有对象只出现一次。

它是如何工作的

清单 1-2 展示了变量声明的基本格式:

type variable;

在声明变量时初始化它们是很常见的,所以您会经常看到:

type variable = initialValue;

字段声明的前面可以有修饰符。例如:

public static variable = initialValue;
protected variable;
private variable;

通常将可见性修饰符——public、protected 或 private——放在第一位,但是您可以随意按任何顺序列出这些修饰符。请注意,随着您对这门语言的深入了解,您将会遇到并需要了解更多的修饰语。默认情况下,如果没有指定修饰符,该类或成员将成为包私有的,这意味着只有包中的其他类才能访问该成员。如果一个类成员被指定为 protected,那么它也是包私有的,除了它在另一个包中的子类也有访问权。

字符串类型在 Java 中是特殊的。它实际上是一个类类型,但是在语法上你可以把它当作一个原始类型。每当您用双引号(" ... ")将字符串括起来时,Java 都会自动创建一个 String 对象).您不需要调用构造函数,也不需要指定 new 关键字。然而,String 是一个类,在该类中有一些方法可供您使用。清单 1-2 末尾显示的 replace()方法就是这样一种方法。

字符串由字符组成。Java 的 char 类型是一个双字节结构,用于以 Unicode-s UTF-16 编码存储单个字符。有两种方法可以生成 char 类型的文本:

  • 如果一个字符很容易输入,那么用单引号将它括起来(例如:G)。

  • 否则,指定以\u 开头的四位数 UTF-16 码位值(例如:' \u0490 ')。

一些 Unicode 码位需要五位数。这些不能用单个 char 值来表示。如果您需要更多关于 Unicode 和国际化的信息,请参阅第十二章。

避免对货币值使用任何原始类型。为此,尤其要避免使用任何一种浮点类型。请参考第十二章及其使用 Java Money API 计算货币金额的方法(方法 12-10)。BigDecimal 在您需要精确的固定十进制算术的任何时候都很有用。

如果您是 Java 新手,您可能不熟悉 String[]数组表示法,如示例中所示。有关阵列的更多信息,请参见第七章。它涵盖了枚举、数组以及通用数据类型。这一章中还有一些例子,展示了如何编写迭代代码来处理值的集合,比如数组。

1-6.从命令行或终端解释器编译和执行

问题

您无法安装 IDE,或者更喜欢使用标准的文本编辑器进行开发。此外,您希望从命令行或终端编译和执行 Java 程序,以便完全控制环境。

解决办法

使用 javac 命令编译您的程序。然后通过 java 命令执行它们。

首先,确保在执行路径中有 JDK 的 bin 目录。您可能需要执行如下命令之一。

Windows:

setx path "%path%;C:\Program Files\Java\jdk1.9.0\bin"

x 轴:

export PATH=/Library/Java/JavaVirtualMachines/jdk1.9.0.jdk/Contents/Home/bin

然后确保您的 CLASSPATH 环境变量包括包含 Java 代码的目录。以下是在 Windows 下设置环境变量的示例:

set CLASSPATH=<<path-to-my-Java>>

现在,将您当前的工作目录更改为与您的项目相对应的目录。菜谱 1-2 让您创建一个名为 Java9Recipes 的项目。在 Windows 系统上更改项目目录,如下所示:

cd <path-to-project>\Java9Recipes

向下一级进入 src 子目录:

cd src

从这里,您可以发出 javac 命令来编译项目中的任何类。将适当的包名作为路径的一部分添加到要编译的每个源文件中。一定要包括。文件名后的 java 扩展名。例如,发出以下命令来编译配方 1-2 中的 HelloWorld 类。

Windows:

javac org\java9recipes\chapter01\recipe1_02\HelloWorld.java

x 轴:

javac org/java9recipes/chapter01/recipe1_02/HelloWorld.java

编译完成后,您将在与相同的目录中拥有一个. class 文件。java 文件。例如,如果您执行目录列表,您应该会看到四个文件:

dir org\java9recipes\chapter01\recipe1_02

HelloMessage.class
HelloWorld.class
HelloMessage.java
HelloWorld.java

编译产生两个文件。一个用于 HelloMessage,另一个用于实现 main()方法的名为 HelloWorld 的类。

通过发出 java 命令调用 Java 虚拟机(JVM)来执行 main()方法。将完全限定的类名作为参数传递给命令。通过在包名前面加上前缀来限定类名,但是这次使用与源文件中相同的点符号。例如:

java org.java9recipes.chapter1.recipe1_02.HelloWorld

不要指定。在命令的末尾初始化。您现在是以类名而不是文件名引用 HelloWorld。您应该会看到与配方 1-2 相同的输出。

小费

人们必须编译源代码。源代码保存在带有. java 后缀的文件中,因此您的操作系统的文件和目录路径符号是合适的。一个执行一个类。类是语言中的一个抽象概念,所以语言的点符号变得很合适。记住这种区别有助于你记住何时使用哪种符号。

它是如何工作的

前两个解决步骤是内务处理步骤。您的执行路径中必须有 Java 编译器和虚拟机。您的程序所使用的任何类也有必要沿着所谓的类路径找到。指定类路径的一种方法是通过 class path 环境变量。有关类路径的更多信息,请参见方法 1-3。

注意

Java 模块化系统为 javac 编译器增加了几个选项。更多信息请参见第二十二章。

末尾没有 c 的命令 java 是用来执行编译好的代码的。将包含 main 方法的类的限定名作为参数传递。JVM 将解释并执行该类中的字节码,从 main 方法开始。JVM 将沿着类路径搜索任何额外需要的类,比如 HelloMessage。

编译器的默认行为是将每个生成的类文件放入保存相应源文件的目录中。您可以通过-d 选项覆盖该行为。例如:

javac -d "<specify-different-location>" "<path-to-project>
\Java9Recipes\src\org\java9recipes\chapter1\recipe1_02\HelloWorld.java"

这个命令中的-d 选项在我们自己的环境中指定一个目录作为保存生成的类文件的目标。该命令还指定源文件的完整路径和文件名。因此,无论当前的工作目录是什么,该命令都可以以相同的结果执行。

小费

配置您的系统,以便您的命令行环境在默认情况下正确设置执行路径和类路径。在基于 Linux 或 Unix 的操作系统中,典型的方法是将适当的命令放入。侧写或者。bash_profile 文件。在 Windows 下,您可以通过单击高级系统设置链接,然后单击环境变量按钮,从名为系统的控制面板窗口中指定环境变量默认值。

有时候,您可能需要为 JVM 的特定执行指定一个定制的类路径。您可以通过-cp 参数来实现,如下所示:

java -cp ".;<path-to-project>\Java9Recipes\build\classes\org\java9recipes\chapter1\recipe1_02"
org.java9recipes.chapter1.recipe1_02.HelloWorld

该执行将首先在当前工作目录(类路径中的前导点)中进行搜索,然后在与 NetBeans 放置已编译类的位置相对应的指定包目录下进行搜索。

注意

有关配置类路径的更多信息,请参见方法 1-3。

1-7.在交互式 jShell 中开发

问题

您希望编写 Java 代码并让它立即被解释,这样您就可以快速地测试、原型化和修改您的代码,而不需要等待编译或编写整个 Java 类的仪式来执行琐碎的任务。

解决办法

通过打开命令提示符或终端,并执行 jshell 实用程序,可以利用 Java 9 中新增的交互式 jShell。jshell 位于您的 JDK 主 bin 目录中,就像 java 和 javac 实用程序一样。假设 /bin 目录在类路径中,那么可以使用如下方式调用 jShell:

jshell
|  Welcome to JShell -- Version 1.9.0
|  Type /help for help

->

一旦解释器启动,就可以在 jShell 会话的生命周期内定义声明,可以立即输入和执行表达式和语句,等等。jShell 还允许 Java 开发人员通过删除多余的结构(如分号)来编写该语言的简写版本。清单 1-5 展示了 jShell 提供的一些基本功能。请记住,当您使用交互式 shell 时,如果您在任何时候需要帮助,可以键入/help 命令。

清单 1-5。交互式 jShell
-> System.out.println("Hello World")
Hello World
-> 1 + 1
|  Expression value is: 2
|    assigned to temporary variable $1 of type int
-> System.out.println("Hello Java 9")
Hello Java 9

-> // working with classes

-> class Main {
>>     // Main method
>>     public static void main(String[] args) {
>>         System.out.println("Classes within jShell");
>>         int index = 0;
>>         while(index <= 10){
>>             System.out.println("Looping: " + index);
>>             index++;
>>         }
>>     }
>> }
|  Added class Main

-> // List classes currently loaded in jShell
-> /classes
|    class Main
-> // Execute Class
-> Main.main(null)
Classes within jShell
Looping: 0
Looping: 1
Looping: 2
Looping: 3
Looping: 4
Looping: 5
Looping: 6
Looping: 7
Looping: 8
Looping: 9
Looping: 10

-> // Reset the state of the jshell
-> /r
|  Resetting state.
-> /classes

-> // Using imports
-> import java.util.ArrayList
-> import java.util.List
-> List<String> colors = new ArrayList<>();
|  Added variable colors of type List<String> with initial value []
-> colors.add("red")
|  Expression value is: true
|    assigned to temporary variable $4 of type boolean
-> colors.add("orange")
|  Expression value is: true
|    assigned to temporary variable $5 of type boolean
-> colors.add("yellow")
|  Expression value is: true
|    assigned to temporary variable $6 of type boolean
-> colors
|  Variable colors of type List<String> has value [red, orange, yellow]

-> // List the current jShell session variables
-> /v
|    List<String> colors = [red, orange, yellow]
|    boolean $4 = true
|    boolean $5 = true
|    boolean $6 = true

-> // List the commands that have been executed
-> /list

   1 : import java.util.ArrayList;
   2 : import java.util.List;
   3 : List<String> colors = new ArrayList<>();
   4 : colors.add("red")
   5 : colors.add("orange")
   6 : colors.add("yellow")
   7 : colors

如前所述,拥有交互式 shell 的一个好处是用于原型代码。在许多情况下,开发人员希望原型化类和对象。清单 1-5 展示了如何将类的代码输入到 jShell 中,以及一些在处理类时有用的命令。在构建原型时,从您最喜欢的编辑器中复制代码并粘贴到 jShell 中,然后针对它执行命令,这通常会很有帮助。

它是如何工作的

jShell 为开发人员提供了一个读取评估打印循环(REPL)环境,用于键入或粘贴代码“片段”并立即执行。与 REPL 环境或其他语言(如 Groovy、Python 和 JRuby)非常相似,jShell 为构建代码原型,甚至是动态执行存储的 Java 代码脚本提供了一个极好的环境。

jShell 允许编写缩写的 Java 代码,也称为代码片段。这可能是有益的,因为它允许人们专注于逻辑,而不是语法。最常用的快捷方式之一是能够在行尾省去分号。为了促进快速原型开发,变量可以在类之外声明,表达式和方法可以在类和接口之外动态键入,并且表达式不会留下副作用。除了动态编写代码的能力之外,jShell 还提供了一个便于在活动会话或实例中添加、修改和删除代码片段的系统。

jShell 环境的一个活动会话或实例构成了一个单独的 JShellState 。JShellState 的一个实例包括所有以前定义的变量、方法、类、导入语句等等,这些都是在同一个 jShell 会话中完成的。一旦 jShell 实例被终止,JShellState 也就结束了,因此所有的声明都丢失了。

有许多助手命令可以输入到 jShell 中,以检索关于当前 JShellState 的信息。/classes 命令列出了已经输入到当前 JShellState 中的所有类。/list 命令列出了已经键入到当前 JShellState 中的所有语句、表达式、类、方法、导入等等。/list 命令在每个清单旁边提供了一个行号,这使得用户可以通过键入/后跟您希望重新执行的行号来轻松地重新执行该行代码。因此,如果希望再次执行第 2 行,可以键入/2 再次执行该行。表 1-1 包含了 jShell 中可用命令的完整列表。

表 1-1。jShell 命令
|

命令

|

描述

|
| --- | --- |
| /l 或/list | 列出在当前会话中键入的源。 |
| /e 或/edit[源的名称或 id] | 打开 JShell 编辑板。可以选择键入要编辑的源条目的名称或 id。 |
| /d 或/drop[源的名称/id] | 删除由名称或 id 引用的源。 |
| /s 或/save [all|history] | 保存在当前会话中键入的源。 |
| /o 或/open | 在 jShell 中打开一个源文件。 |
| /v 或/vars | 列出当前会话中已声明的变量及其当前值。 |
| /m 或/methods | 列出已在当前会话中声明的方法。 |
| /c 或/classes | 列出当前会话中已声明的类。 |
| /x 或/exit | 退出当前 jShell 会话。 |
| /r 或/reset | 重置当前会话的 JShellState。 |
| /f 或/feedback [level] | 启动反馈-选项包括(关闭、简洁、正常、详细、默认或?). |
| /p 或/prompt | 切换提示在 shell 中的显示。 |
| /cp 或/class path[路径] | 将类型化路径添加到当前类路径中。 |
| /h 或/history | 列出活动 JShellState 的历史记录。 |
| /setstart [文件] | 读取和设置启动定义文件。 |
| /savestart[文件] | 将当前会话的定义保存到指定的启动文件。 |
| /! | 重新执行最后一段代码。 |
| / | 重新执行第 n 段代码。 |
| /- | 重新执行第 n 个先前的代码段。 |

如果您键入/e 命令,一个名为“JShell Edit Pad”的便笺式编辑器将会打开,其中包含您为当前 JShellState 输入的源代码,如图 1-6 所示。您可以在这个面板中编辑源代码,然后单击“Accept”按钮在 jShell 中评估这些源代码。

A323910_3_En_1_Fig6_HTML.jpg

图 1-6。jshell edit pad(jshell 编辑面板)

jShell 的其他有用特性是,您可以通过按键盘上的向上箭头调出之前键入的命令。交互式 shell 还具有 tab 补全功能。如果您开始键入一条语句,然后按 Tab 键,该语句将自动完成,或者显示当前键入字符的选项列表。还可以设置一个预定义的导入列表,以便每次启动 jShell 会话时,这些导入都会自动发生。

jShell 提供了一个交互式环境,允许在输入代码片段时立即得到反馈。这对于原型开发或学习语言是有益的。其他语言,比如 Groovy、Python 和 Scala,也有类似的 REPL 环境。现在,jShell 可用于 Java,它为课堂使用的更具交互性的环境打开了大门,并提高了开发人员的原型开发效率。

小费

要了解 jShell 中可用命令的更多信息,只需在 Shell 打开后键入/help。help 特性显示了 jShell 中可用特性的详细列表。

1-8.与字符串相互转换

问题

您有一个存储在原始数据类型中的值,并且您希望将该值表示为人类可读的字符串。或者,您想从另一个方向出发,将人类可读的字符串转换为原始数据类型。

解决办法

遵循清单 1-6 中的一个模式。该清单展示了从字符串到双精度浮点值的转换,并展示了返回字符串的两种方法。

清单 1-6。字符串转换的一般模式
package org.java9recipes.chapter01.recipe1_08;

public class StringConversion {
    public static void main (String[] args) {
        double pi;
        String strval;

        pi = Double.parseDouble("3.14");
        System.out.println(strval = String.valueOf(pi));
        System.out.println(Double.toString(pi));
    }
}

它是如何工作的

该解决方案展示了一些适用于所有基本类型的转换模式。首先,将浮点数从人类可读的表示形式转换成 Java 语言用于浮点运算的 IEEE 754 格式:

pi = Double.parseDouble("3.14");

注意模式。您可以用 Float、Long 或任何其他目标数据类型替换 Double。每个基本类型都有一个对应的包装类,名称相同,但首字母大写。这里的基元类型是 double,对应的包装器也是 Double。包装器类实现了 Double.parseDouble()、Long.parseLong()、Boolean.parseBoolean()等辅助方法。这些解析方法将人类可读的表示转换成相应类型的值。

反过来说,调用 String.valueOf()通常是最容易的。String 类实现了这个方法,并且为每个基本数据类型重载了这个方法。或者,包装类也实现 toString()方法,您可以调用该方法将基础类型的值转换为人类可读的形式。至于采取哪种方法,这是你自己的喜好。

以数值类型为目标的转换需要一些异常处理才能实现。您通常需要很好地适应这样一种情况,即字符串值应该是有效的数字表示,但事实并非如此。第九章详细介绍了异常处理,接下来的食谱 1-10 提供了一个简单的例子来帮助你开始。

警告

布尔类型的文字为“真”和“假”。它们区分大小写。当使用 Boolean parseBoolean()转换方法从字符串进行转换时,除这两个值之外的任何值都会被默认解释为 false。

1-9.通过命令行执行传递参数

问题

您希望将值传递给一个 Java 应用,该应用是通过命令行使用 java 实用程序调用的。

解决办法

使用 java 实用程序运行应用,并在应用名称后指定要传递给它的参数。如果你要传递多个参数,每个参数之间应该用空格隔开。例如,假设您想将参数传递给清单 1-7 中创建的类。

清单 1-7。访问命令行参数的示例
package org.java9recipes.chapter01.recipe1_09;

public class PassingArguments {
    public static void main(String[] args){
        if(args.length > 0){
            System.out.println("Arguments that were passed to the program: ");
            for (String arg:args){
                System.out.println(arg);
            }
        } else {
            System.out.println("No arguments passed to the program.");
        }
    }
}

首先,确保编译程序,以便有一个. class 文件可以执行。您可以在 NetBeans 中通过右键单击该文件并从上下文菜单中选择“编译文件”选项,或者通过命令行或终端中的 javac 实用程序来完成此操作。

接下来,打开命令提示符或终端窗口,遍历到项目的 build\classes 目录。(关于从命令行执行的详细讨论,请参见配方 1-6)。例如:

cd <path-to-project>\Java9Recipes\build\classes

现在发出一个 java 命令来执行该类,并在命令行中类名后面键入一些参数。以下示例传递两个参数:

java org.java9recipes.chapter01.recipe1_09.PassingArguments Upper Peninsula

您应该会看到以下输出:

Arguments that were passed to the program:
Upper
Penninsula

空格分隔参数。如果要传递包含空格或其他特殊字符的参数,请用双引号将字符串括起来。例如:

java org.java9recipes.chapter01.recipe1_09.PassingArguments "Upper Peninsula"

输出现在只显示一个参数:

Arguments that were passed to the program:
Upper Penninsula

双引号将字符串“Upper Peninsula”转换为单个参数。

它是如何工作的

所有可从命令行或终端执行的 Java 类都包含一个 main()方法。如果查看 main()方法的签名,可以看到它接受 String[]参数。换句话说,您可以将 String 对象的数组传递给 main()方法。命令行解释程序(如 Windows 命令提示符和各种 Linux 和 Unix shells)从命令行参数中构建一个字符串数组,并代表您将该数组传递给 main()方法。

示例中的 main()方法显示传递的每个参数。首先,测试名为 args 的数组的长度,看它是否大于零。如果是,该方法将通过执行 for 循环来遍历数组中的每个参数,同时显示每个参数。如果没有传递任何参数,args 数组的长度将为零,并显示一条消息。否则,您会看到一条不同的消息,后跟一个参数列表。

命令行解释器将空格和其他字符识别为分隔符。一般来说,将数值作为由空格分隔的参数传递是安全的,不需要用引号将每个值括起来。但是,您应该养成用双引号括住字符串参数的习惯,如最终的解决方案示例所示。这样做是为了消除关于每个参数的开始和结束的任何模糊性。

注意

Java 将所有参数视为字符串。如果您将数值作为参数传递,它们将以人类可读的形式作为字符串输入 Java。您可以使用配方 1-8 中所示的转换方法将它们转换成适当的数值类型。

1-10.通过 jShell 执行脚本

问题

您希望编写一个原型或脚本,并通过 jShell 实用程序从命令行或终端执行它。

解决办法

虽然 jShell 并不打算为 Java 开发提供一种新的语言语法,但是可以将在 jShell 中执行的源代码片段保存到一个文件中,然后将该文件传递给 jShell 实用程序来执行。在这个解决方案中,我们将一个简单的代码片段保存到一个名为 myScript.java 的文件中,并使用 jShell 实用程序执行它。

首先,将下面的源代码保存到一个名为 myScript.java 的文件中,并保存到您的文件系统中。

System.out.println("Hello from jShell")
/x

使用以下语法执行脚本:

jShell <path-to-file>/myScript.java

输出:

Hello from jShell

它是如何工作的

有时,使用文本编辑器或 jShell 编辑板(见配方 1-7)来保存可以在 JShell 环境中执行的源代码是有益的。这增加了快速构建代码原型的能力,也促进了开发可反复执行的脚本的能力。这对于开发可由 JVM 执行的调度任务或管理任务非常有用。因此,jShell 的源代码可以存储在包含您选择的扩展名的文件中,然后该文件可以传递给 jShell 执行。

在该解决方案中,输出一个简单的字符串,然后退出 jShell 环境。请注意,/x 命令位于文件中源代码之后的单独一行。/x 命令告诉 jShell 环境在完成后退出。如果在完成时退出,一旦源代码运行完成并且 jShell 环境关闭,文件中定义的任何变量、方法、类等都将丢失。

不建议使用 jShell 环境编写应用来执行。事实上,GUI 应用超出了 jShell 的范围,调试器也不受支持。该环境显然是为了教育和原型设计的目的。然而,有些人可能会发现保存代码片段以便以后通过 jShell 执行是很方便的。

1-11.接受来自键盘的输入

问题

您对编写一个接受用户键盘输入的命令行或终端应用感兴趣。

解决办法

利用 java.io.BufferedReader 和 java.io.InputStreamReader 类读取键盘输入并将其存储到局部变量中。清单 1-8 显示了一个程序,它会一直提示输入,直到您输入一些代表 long 类型有效值的字符。

清单 1-8。键盘输入和异常处理
package org.java9recipes.chapter01.recipe1_11;

import java.io.*;

public class AcceptingInput {
    public static void main(String[] args){
        BufferedReader readIn = new BufferedReader(
                new InputStreamReader(System.in)
        );
        String numberAsString = "";
        long numberAsLong = 0;
        boolean numberIsValid = false;
        do {
            /* Ask the user for a number. */
            System.out.println("Please enter a number: ");
            try {
                numberAsString = readIn.readLine();
                System.out.println("You entered " + numberAsString);
            } catch (IOException ex){
                System.out.println(ex);
            }

            /* Convert the number into binary form. */
            try {
                numberAsLong = Long.parseLong(numberAsString);
                numberIsValid = true;
            } catch (NumberFormatException nfe) {
                System.out.println ("Not a number!");
            }
        } while (numberIsValid == false);  
    }
}

以下是该程序的运行示例:

Please enter a number:
No
You entered No
Not a number!
Please enter a number:
Yes
You entered Yes
Not a number!
Please enter a number:
42
You entered 42
BUILD SUCCESSFUL (total time: 11 seconds)

前两个输入不代表 long 数据类型中的有效值。第三个值有效,运行结束。

它是如何工作的

我们的应用经常需要接受某种类型的用户输入。诚然,现在大多数应用都不是从命令行或终端使用的,但是能够创建从命令行或终端读取输入的应用有助于打下良好的基础,并且在一些应用或脚本中可能是有用的。终端输入在开发您或系统管理员可能使用的管理应用时也很有用。

这个配方的解决方案中使用了两个助手类。它们是 java.io.BufferedReader 和 java.io.InputStreamReader。使用这些类的代码的早期部分理解起来特别重要:

BufferedReader readIn = new BufferedReader(
        new InputStreamReader(System.in)
);

这个语句中最里面的对象是 System.in,它代表键盘。您不需要声明 System.in. Java 的运行时环境会为您创建对象。它简单易用。

System.in 提供对来自输入设备的原始数据字节的访问,在我们的例子中,输入设备是键盘。InputStreamReader 类的工作是获取这些字节并将它们转换为当前字符集中的字符。System.in 被传递给 InputStreamReader()构造函数以创建 InputStreamReader 对象。

InputStreamReaderknows 了解字符,但不了解行。BufferedReader 类的工作是检测输入流中的换行符,并使您能够方便地一次读取一行。BufferedReader 还通过允许从输入设备以不同大小的块进行物理读取来提高效率,这些块的大小不同于应用消耗数据的大小。当输入流是一个大文件而不是键盘时,这一点会有所不同。

下面是清单 1-8 中的程序如何利用 BufferedReader 类的一个实例(名为 readIn)从键盘读取一行输入:

numberAsString = readIn.readLine();

执行此语句会触发以下序列:

  1. System.in 返回一个字节序列。

  2. InputStreamReader 将这些字节转换为字符。

  3. BufferedReader 将字符流分成多行输入。

  4. readLine()向应用返回一行输入。

I/O 调用必须包装在 try-catch 块中。这些块用于捕捉任何可能发生的异常。如果转换不成功,示例中的 try 部分将会失败。失败会阻止将 numberIsValid 标志设置为 true,这将导致 do 循环进行另一次迭代,以便用户可以再次尝试输入有效值。要了解更多关于捕捉异常的信息,请参见第九章。

清单 1-8 顶部的以下语句值得一提:

import java.io.*;

该语句使 java.io 包中定义的类和方法可用。其中包括 InputStreamReader 和 BufferedReader。还包括在第一个 try-catch 块中使用的 IOException 类。

1-12.记录您的代码

问题

您希望记录一些 Java 类,以帮助将来的维护。

解决办法

使用 Javadoc 将注释放在要记录的任何类、方法或字段之前。要开始这样的注释,请写下字符/**。然后用星号()开始后面的每一行。最后,用字符/结束注释。清单 1-9 显示了用 Javadoc 注释的方法。

清单 1-9。Javadoc 表单中的注释
package org.java9recipes.chapter01.recipe1_12;

import java.math.BigInteger;

public class JavadocExample {
    /**
     * Accepts an unlimited number of values and
     * returns the sum.
     *
     * @param nums Must be an array of BigInteger values.
     * @return Sum of all numbers in the array.
     */
     public static BigInteger addNumbers(BigInteger[] nums) {
         BigInteger result = new BigInteger("0");
         for (BigInteger num:nums){
             result = result.add(num);
         }

         return result;
     }
    /**
     * Test the addNumbers method.
     * @param args not used
     */
     public static void main (String[] args) {
         BigInteger[] someValues = {BigInteger.TEN, BigInteger.ONE};
         System.out.println(addNumbers(someValues));
     }
}

可以用同样的方法将注释添加到类和字段的开头。这些注释对您和维护代码的其他程序员很有帮助,并且它们的特定格式使您能够轻松地生成代码的 HTML 引用。

通过调用名为 Javadoc 的工具生成 HTML 引用。这是一个命令行工具,它解析命名的 Java 源文件,并根据定义的类元素和 Javadoc 注释制定 HTML 文档。例如:

javadoc JavadocExample.java

这个命令将生成几个 HTML 文件,包含类、方法和字段的文档。如果源代码中不存在 Javadoc 注释,仍然会生成一些默认文档。要查看文档,请将以下文件加载到浏览器中:

index.html

该文件将与您正在记录的类或包位于同一目录中。还将有一个 index-all.html 文件,严格按照字母顺序列出记录在案的实体。

请记住,使用 Javadoc 工具和使用 javac 时适用相同的规则。您必须驻留在与源文件相同的目录中,或者在文件名前面加上文件所在的路径。

它是如何工作的

从头开始为应用生成文档可能非常繁琐。维护文档可能更麻烦。JDK 附带了一个称为 Javadoc 的广泛的文档系统。在您的代码源代码中放置一些特殊的注释,并运行一个简单的命令行工具,可以很容易地生成有用的文档并使其保持最新。此外,即使应用中的某些类、方法或字段没有专门针对 Javadoc 实用程序进行注释,也会为这些元素生成默认文档。

格式化文档

要创建 Javadoc 注释,请以字符/**开头。虽然从 Java 1.4 开始是可选的,但是通常的做法是在注释中包含一个星号作为每一个后续行的第一个字符。另一个好的做法是缩进注释,使其与被记录的代码一致。最后,用字符*/结束注释。

Javadoc 注释应该以对类或方法的简短描述开始。很少使用 Javadoc 对字段进行注释,除非它们被声明为 public static final (constants),在这种情况下,提供注释是个好主意。一个注释可以有几行的长度,甚至可以包含多个段落。如果你想把注释分成几个段落,那么使用

标签来分隔这些段落。注释可以包括几个标记,这些标记指示关于被注释的方法或类的各种细节。Javadoc 标签以&符号(@)开始,一些常见的标签如下:

@param: Name and description of a parameter
@return: What is returned from the method
@see: Reference to another piece of code

您还可以在 Javadoc 中包含内联链接来引用 URL。要包含内联链接,请使用标签{@link My link},其中 Link 是您要指向的实际 URL,My Link 是您要显示的文本。在 Javadoc 注释中还可以使用许多其他标记,包括{@literal}、{@code}、{@value org}和许多其他标记。有关完整的列表,请参见 OTN 网站上的 Javadoc 参考资料。

执行工具

Javadoc 工具也可以针对整个包或源代码运行。只需将包名传递给 Javadoc 工具,而不是单个的源文件名。例如,如果一个应用包含一个名为 org.juneau.beans 的包,那么该包中的所有源文件都可以通过运行该工具进行记录,如下所示:

javadoc org.juneau.beans

要一次为多个包生成 Javadoc,请用空格分隔包名,如下所示:

javadoc org.juneau.beans org.juneau.entity

另一个选项是使用–source path 标志指定源文件的路径。例如:

javadoc –sourcepath /java/src

默认情况下,Javadoc 工具将生成 HTML,并将其放入与被记录的代码相同的包中。如果您喜欢将源文件从文档中分离出来,那么这个结果可能会变成一个混乱的噩梦。相反,您可以通过将–d 标志传递给 Javadoc 工具来为生成的文档设置一个目的地。

1-13.读取环境变量 s

问题

您正在开发的应用需要利用一些环境变量。您希望从操作系统级别读取已设置的值。

解决办法

利用 Java 系统类来检索任何环境变量值。System 类有一个名为 getenv()的方法,它接受与系统环境变量的名称相对应的字符串参数。然后,该方法将返回给定变量的值。如果不存在匹配的环境变量,将返回空值。清单 1-10 提供了一个例子。ReadOneEnvVariable 类接受环境变量名作为参数,并显示已经在操作系统级别设置的变量值。

清单 1-10。读取环境变量的值
package org.java9recipes.chapter1.recipe1_13;

public class ReadOneEnvVariable {
    public static void main(String[] args) {
        if (args.length > 0) {
            String value = System.getenv(args[0]);
            if (value != null) {
                System.out.println(args[0].toUpperCase() + " = " + value);
            } else {
                System.out.println("No such environment variable exists");
            }
        } else {
            System.out.println("No arguments passed");
        }
    }
}

如果您对检索系统上定义的环境变量的完整列表感兴趣,请不要向 System.getenv()方法传递任何参数。您将收到一个 Map 类型的对象,其中包含所有的值。您可以遍历它们,如清单 1-11 所示。

清单 1-11。遍历环境变量的映射
package org.java9recipes.chapter1.recipe1_13;

import java.util.Map;

public class ReadAllEnvVariables {
    public static void main(String[] args){
        if(args.length > 0){
            String value = System.getenv(args[0]);
        if (value != null) {
            System.out.println(args[0].toUpperCase() + " = " + value);
        } else {
            System.out.println("No such environment variable exists");
        }
        } else {
            Map<String, String> vars = System.getenv();
            for(String var : vars.keySet()){
                System.out.println(var + " = " + vars.get(var));
            }
        }
    }
}

它是如何工作的

System 类包含许多不同的实用程序,可以帮助应用开发。其中之一是 getenv()方法,它将为给定的系统环境变量返回值。

您还可以返回所有变量的值,在这种情况下,这些值存储在一个映射中。映射是名称/值对的集合。第七章提供了更多关于地图的信息,包括一个详细展示如何迭代地图的方法。

清单 1-10 和 1-11 中获取环境变量值的方法是相同的。它被重载来处理解决方案中显示的两种情况。如果您只想获取变量值,请将变量名作为字符串传递。不传递任何参数来获取当前设置的所有变量的名称和值。

摘要

这一章包括了允许你快速开始使用 Java 的方法。它涵盖了 JDK 的安装,以及 NetBeans IDE 的安装和使用。本章还介绍了一些基础知识,如声明变量、编译代码和文档。本书的其余部分深入探讨了 Java 语言的各个不同领域,涵盖了从初学者到专家的各种主题。当您完成本书剩余部分中的示例时,请参考本章了解配置细节。

二、Java 9 增强

JDK 的每个版本都为 Java 平台带来了新的增强和功能。每个版本还具有与以前版本的向后兼容性。这本书包括了许多介绍 Java 9 新特性的方法,这一章展示了几个最重要的增强来吊起你的胃口。本章绝不是所有 Java 9 增强的完整列表。相反,它是让您了解 Java 9 的一些热门新特性的一个飞跃。

2-1.避免接口代码中的冗余

问题

您希望在一个包含非常相似的代码的接口中实现两个或更多的默认方法。与其将代码复制到每个不同的默认方法中并单独维护每个默认方法,不如将相似的代码封装到它自己的方法中以便重用。

解决办法

利用接口中的私有方法来缓解这个问题。Java 9 提供了在接口中包含私有方法的能力。私有方法仅在该接口内可用,并且不能由实现该接口的任何类使用。但是,作为接口一部分的每个默认方法实现都可以利用私有方法。

下面的接口包括两个默认方法和一个私有方法。私有方法封装了可以在每个默认方法实现中使用的功能。

public interface Pool {
    /**
     * Calculate volume (gal) for a fixed depth square or rectangular pool.
     */
    public default double squareOrRectConstantDepth(double length, double width, double depth){
        return volumeCalc(length, width, depth);
    }
    /**
     * Calculate volume (gal) for a variable depth square or rectangular pool.
     */
    public default double squareOrRectVariableDepth(double length, double width,
                                                    double shallowDepth, double middleDepth,
                                                    double deepDepth){
        double avgDepth = (shallowDepth + middleDepth + deepDepth) / 3;
        return volumeCalc(length, width, avgDepth);
    }
    /**
     * Standard square or rectangular volume calculation.
     */
    private double volumeCalc(double length, double width, double depth){
        return length * width * depth * 7.5;
    }
}

它是如何工作的

在 Java 8 之前,不可能在 Java 接口中包含代码实现。接口是 Java 中的引用类型,类似于类。然而,它的初衷只允许抽象方法、常量、静态方法和嵌套类型。因此,实现接口的类必须实现每个抽象方法。在 Java 8 中,这个限制被取消了,以默认方法的形式包含方法实现成为可能。默认方法可以包含接口中的实现,或者它的实现可以被实现类重写。因此,命名为 default method,意味着如果实现类没有提供默认方法实现,则默认方法实现驻留在接口中。接口中不允许私有方法。

出现了这样的情况:一个接口中的多个默认方法可能包含相似的代码。这段代码现在可以封装在接口的私有方法实现中。私有方法实现不能在接口外部使用。它只能由同一接口中包含的任何默认方法使用。在这个配方的解决方案中,volumeCalc()方法使用标准公式返回方形或矩形游泳池的计算体积。界面中的每个默认方法都能够利用 volumeCalc()方法来查找体积。但是,volumeCalc()方法不能在界面之外使用。

这似乎是一个有争议的话题,因为接口最初只用于字段和方法声明,但也可以说在许多默认方法实现中复制相同的代码是一种不好的做法。尽管如此,这个特性使得在一个接口中重用代码变得更加容易,从而减少了出错的机会,并使维护变得更加容易。

2-2.为简化和代码重用创建模块

问题

您正在编写一个实用程序库或 Java 应用,并且您不希望依赖类路径来管理与其他库的依赖关系。此外,您希望将您的库打包,以便它可以轻松地集成到其他项目中。

解决办法

将您的库或应用开发为一个模块。创建模块非常容易。然而,模块本身可能变得非常复杂。这个例子将涵盖一个非常简单的模块的创建,它不依赖于任何其他模块。其他模块也不会依赖于该模块。首先在文件系统的某个地方创建一个新目录…在这种情况下,将其命名为“recipe2-2”在其中创建一个名为 src 的新文件夹,然后在 src 文件夹中创建一个名为 module-info.java 的文件,这是模块描述符。在该文件中,列出模块名称,如下所示:

module org.acme {}

接下来,在之前创建的 src 目录中创建一个名为 org.acme.wordcount 的文件夹(图 2-1 )。接下来,在 org.acme.wordcount 文件夹中创建一个名为 org 的文件夹。随后,在 org 文件夹中创建一个 acme 文件夹,然后在 acme 文件夹中创建一个 wordcount 文件夹。

A323910_3_En_2_Fig1_HTML.jpg

图 2-1。模块文件夹层次结构

现在,通过在 wordcount 文件夹中添加一个名为 WordCount.java 的新文件来创建模块的主体。将以下代码放在 WordCount.java 文件中:

package org.acme.wordcount;
public class WordCount {

    public static void main(String[] args) {
        int counter = 0;
        if (args.length > 0){
            for(String arg:args){
                System.out.println("Position " + counter + ": " + arg.length());
                counter++;
            }
        }
    }    
}

通过使用命令行或终端并遍历前面创建的 src 目录,利用 javac 实用程序来编译模块。发出 javac 命令,指定-d 标志来列出编译后的代码将放入的文件夹。列出每个要编译的源文件,包括 module-info.java 描述符,用空格隔开。以下命令编译在中开发的源代码,并将结果放入名为 mods/org.acme.wordcount 的目录中。

javac -d src/mods/org.acme.wordcount src/module-info.java src/org.acme.wordcount/org/acme/wordcount/WordCount.java

现在代码已经编译好了,是时候执行模块了。使用 java 可执行文件,指定- module-path 选项(这是 Java 9 中的新增功能)来指示模块源的路径。-m 选项用于指定模块的主类。遍历 src 目录并发出以下命令:

java --module-path mods -m org.acme.wordcount/org.acme.wordcount.WordCount testing one two three

此示例将单词“testing”、“one”、“two”、“three”传递给要计数的模块。输出应该如下所示:

Position 0: 7
Position 1: 3
Position 2: 3
Position 3: 5

它是如何工作的

Jigsaw 项目为 Java 平台带来了模块,最终引入了一种方法来消除旧的类路径,并使用更新的、更可插拔的架构。Java 9 模块系统允许封装自包含的代码模块,并使它们通用,这样一个模块可以依赖于其他模块,或者另一方面,其他模块可以依赖于它。这种模块化的依赖关系取代了旧的类路径系统,尽管类路径仍然可以用来适应向后兼容性,也适用于模块化没有什么意义的情况。

模块的创建由 module-info.java 描述符文件组成。该文件用于指示包含该模块的包,以及该模块与其他模块共享的依赖契约。请参见第二十二章,了解更多关于描述符文件的详细信息。

这个菜谱中的自包含应用驻留在 org.acme.wordcount.WordCount.java 文件中,可以用 javac 编译,用 java 可执行文件执行,这是可以想象的。这两个实用程序提供了新的选项来支持模块化,并且这个方法演示了如何使用这些新选项来编译和执行模块。有关模块编译和执行的更多细节,请参见配方 22-2。

2-3.轻松检索操作系统进程信息

问题

您希望能够找到有关操作系统进程的信息。

解决办法

利用 Java 9 中更新的流程 API。新的 ProcessHandle 接口允许用户轻松获取有关操作系统进程的信息。在下面的代码中,所有操作系统进程都被列出并输出到命令行。

public static void listProcesses(){
    ProcessHandle.allProcesses()
            .forEach(System.out::println);
}

然而,这不是很有帮助,因为它只是简单地列出了每个操作系统进程的进程号…这不是很有用。为了获得流程的更多细节,我们需要获得 ProcessHandle 并调用它的助手方法,这很容易做到。在打印 ProcessHandle 时,下面的代码将打印关于每个进程的更多信息。信息本身。

public static void detailListProcesses(){
    ProcessHandle.allProcesses()
            .forEach(h->System.out.println(formattedProcess(h)));
}

public static String formattedProcess(ProcessHandle handle){
        long pid = handle.getPid();
        boolean alive = handle.isAlive();
        Optional<Duration> cpuDuration = handle.info().totalCpuDuration();
        Optional<String> handleName = handle.info().command();
        return pid + " " + alive + " " + handleName + ":"+ cpuDuration;
     }

示例输出可能如下所示:

17584 true Optional[/Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home/bin/java]:Optional[PT0.250501S]
17581 true Optional[/Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home/bin/java]:Optional.empty
17576 true Optional.empty:Optional.empty
17575 true Optional.empty:Optional.empty
17574 true Optional.empty:Optional.empty
17364 true Optional[/System/Library/Frameworks/CoreServices.framework/Frameworks/Metadata.framework/Versions/A/Support/mdworker]:Optional.empty
17247 true Optional[/Applications/Google Chrome.app/Contents/Versions/56.0.2924.87/Google Chrome Helper.app/Contents/MacOS/Google Chrome Helper]:Optional.empty

如果您希望检索与运行该流程的用户相关的信息,这也很容易做到。

public static void detailListProcessUsers(){
    ProcessHandle.allProcesses()
            .forEach(h->System.out.println(listOsUser(h)));
}

public static String listOsUser(ProcessHandle handle){
    ProcessHandle.Info procInfo = handle.info();
    return handle.getPid() + ": " +procInfo.user();
}

使用这种技术的示例输出可能如下所示:

17584: Optional[Juneau]
17581: Optional[Juneau]
17576: Optional[_postfix]
17575: Optional[_postfix]
17574: Optional[root]

它是如何工作的

在 Java 9 发布之前,获取有关操作系统进程的信息很麻烦。我们必须使用 management factory . getruntimemxbean()方法获取进程 id,然后解析返回的字符串。Java 9 中引入了 ProcessHandle 接口,使得操作系统进程信息的检索成为 JDK 的一等公民。表 2-1 显示了可以在 ProcessInfo 中调用以检索所需信息的方法。

表 2-1。ProcessHandle 接口
|

方法

|

描述

|
| --- | --- |
| 所有进程() | 对当前进程可见的所有进程的快照。 |
| 儿童() | 当前进程的子进程的快照。 |
| compareTo(ProcessHandle) | 将一个 ProcessHandle 与另一个进行比较。 |
| 当前() | 返回当前进程的 ProcessHandle。 |
| 后代() | 当前进程的所有后代的快照。 |
| 销毁() | 请求终止该进程。返回一个布尔值来表示结果。 |
| destroyForcibly() | 请求强制终止进程。返回一个布尔值来表示结果。 |
| 等于(对象) | 如果传入的对象不为空,则返回 true,并表示相同的系统进程,否则返回 false。 |
| getPid() | 返回进程的进程 ID。 |
| hashCode() | 返回进程的哈希代码值。 |
| 信息() | 返回 ProcessHandle。Info,它是关于当前进程的信息的快照。 |
| isalive() | 返回一个布尔值,以指示进程是否处于活动状态。 |
| (长)的 | 为现有的本地进程返回可选的 |
| onExit() | 为进程的终止返回 CompletableFuture 。然后可以调用 CompletableFuture 来确定状态。 |
| 父项() | 为当前进程的父进程返回可选的 |
| 支持 NormalTermination() | 如果 destroy()的实现将正常终止进程,则返回 true。 |

2-4.轻松处理错误

问题

你想容易地管理有效的最终变量的关闭。

解决办法

try-with-resources 构造是在 Java 7 中引入的,它允许轻松管理资源。在 Java 9 中,这变得更加容易,因为不需要为了构造而有效地创建新变量。在下面的代码中,writeFile()方法将 BufferedWriter 作为参数,由于它被传递到方法中并准备使用,因此它实际上是 final。这意味着它可以简单地列在 try-with-resources 中,而不是创建一个新的变量。

public static void main(String[] args) {
    try {
        writeFile(new BufferedWriter(
                new FileWriter("Easy TryWithResources")),
                "This is easy in Java 9");
    } catch (IOException ioe) {
        System.out.println(ioe);
    }
}

public static void writeFile(BufferedWriter writer, String text) {
    try (writer) {
        writer.write(text);
    } catch (IOException ioe) {
        System.out.println(ioe);
    }
}

在 Java 9 之前,writeFile 应该如下所示:

public static void writeFile(BufferedWriter writer, String text) {
   try (BufferedWriter w = writer) {
        w.write(text);
    } catch (IOException ioe) {
        System.out.println(ioe);
    }
}

这段代码将创建一个名为“Easy TryWithResources”的新文件,并将文本“This is easy in Java 9”放入该文件。

它是如何工作的

在 Java 9 中,try-with-resources 构造变得更加容易。try-with-resources 构造是在 Java 8 中引入的,它允许人们非常容易地处理资源的打开和关闭。如果我们有一个资源,比如数据库连接或 BufferedStream,明智地管理是一个好主意。换句话说,打开资源,然后相应地使用它,最后在完成时关闭资源,以确保没有资源泄漏。try-with-resources 构造允许在 try 块中打开一个资源,并在该块完成后自动清除它。

在解决方案中,显示了处理资源的原始方式,随后是 Java 9 中的新方式。现在,如果将资源作为参数传递给一个方法,或者如果它是一个 final 字段,就可以简单地开始使用 try-with-resources 构造中的资源。这意味着不再需要为了在 try-with-resources 中使用而创建占位符变量。虽然这不是一个主要的语言变化,但它肯定会使处理资源变得更容易,而且它肯定会使 try-with-resources 块更容易理解。

2-5.使用流过滤条件前后的数据

问题

您希望利用流来有效地操作您的集合。这样做时,您希望在特定条件发生之前和/或之后过滤这些流。最后,您希望在满足给定的谓词条件之前检索集合中的所有数据。您还希望检索满足给定谓词条件后放置的集合中的所有数据。

解决办法

在您的流中利用新的 Java 9 takeWhile()和 dropWhile()构造。假设我们有以下数据集合,我们希望检索包含单词“Java”的元素之前的所有元素。

List<String> myLangs = Arrays.asList("Jython is great","Groovy is awesome",
"Scala is functional", "JRuby is productive","Java is streamlined","","Kotlin is interesting");

要检索包含字符串“Java”的元素之前的所有元素,我们可以使用 takeWhile()构造,如下所示:

Stream.of("Jython is great","Groovy is awesome","Scala is functional",
                "JRuby is productive","Java is streamlined","","Kotlin is interesting")
                .takeWhile(s -> !s.contains("Java"))
                .forEach(System.out::println);

假设我们希望检索包含字符串“Java”的元素之后出现的所有元素。我们可以使用 dropWhile()构造,如下所示:

Stream.of("Jython is great","Groovy is awesome","Scala is functional",
                "JRuby is productive","Java is streamlined","","Kotlin is interesting")
                .dropWhile(s -> !s.contains("Java"))
                .forEach(System.out::println);

它是如何工作的

流改变了我们在 Java 中开发代码和处理数据集合的方式。可用于流的原始过滤器集相当丰富。然而,在 Java 8 中,增加了更多的选项,使得用流来提炼数据变得更加容易。takeWhile()和 dropWhile()构造允许对流进行解析,返回一个包含第一个不符合指定谓词条件的元素之前的所有元素的新流,或者返回一个包含第一个不符合指定谓词条件的元素之后的所有元素的新流。

在这个菜谱的解决方案中,解析字符串列表,第一次将每个元素打印到命令行。然后,takeWhile()构造被应用于相同的字符串流和流中的元素,然后不满足指定条件的元素将被打印到命令行。takeWhile()接受一个谓词条件,然后将其应用于流中的每个元素,然后只返回那些在谓词条件不匹配之前被迭代的元素。位于流中不满足条件的位置及其之后的所有元素都不会被返回。

使用 dropWhile()构造时会出现相反的结果。在解决方案中,所有流元素都将被忽略,直到返回不再满足指定条件的第一个元素。流中的每个后续元素也将被返回。

takeWhile 和 dropWhile 构造与过滤器非常相似,只是只有一个失败的条件会导致其余的元素分别被忽略或返回。

2-6.开发一个简洁的 HTTP 客户端

问题

您希望在 Java 应用中开发一个 HTTP 客户端。

解决办法

使用 Java 9 的更新的 HTTP/2 客户端。在下面的例子中,通过 HTTP 客户端代码解析并返回一个网站。在下面的例子中,我们的新闻网页以字符串的形式返回。

HttpResponse r1;
try {
    r1 = HttpRequest.create(new URI("http://www.apress.com/us/"))
            .GET()
            .response();

    int responseCode = r1.statusCode();
    if(responseCode == 200){
        System.out.println(r1.body(asString()));
    }

 } catch (URISyntaxException|IOException|InterruptedException ex) {
    // Log error
}

输出如下所示(为简洁起见,略作缩写):

<!DOCTYPE html>
<!--[if lt IE 7]> <html lang="en" class="no-js ie6 lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 7]> <html lang="en" class="no-js ie7 lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html lang="en" class="no-js ie8 lt-ie9"> <![endif]-->
<!--[if IE 9]> <html lang="en" class="no-js ie9"> <![endif]-->
<!--[if gt IE 9]><!--> <html lang="en" class="no-js"> <!--<![endif]-->
<head><meta http-equiv="x-ua-compatible" content="IE=edge">
<script type="text/javascript" src="//static.springer.com/spcom/js/vendor/googleapis/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script type="text/javascript" src="//cdn.optimizely.com/js/8200882355.js"></script>
<script type="text/javascript" id="angular-script" src="//static.springer.com/spcom/js/vendor/googleapis/ajax/libs/angularjs/1.2.17/angular.min.js"></script>
<script type="text/javascript" id="script--1908162026" src="//static.springer.com/spcom/min/prod.js?r=201702071421-9"></script>
<link rel="stylesheet" type="text/css" href="//static.springer.com/spcom/min/modern_sprcom-cms-frontend_apress.css?r=201702071421-9" />
<!--[if (lt IE 9) & (!IEMobile)]><link rel="stylesheet" type="text/css" href="//static.springer.com/spcom/min/ielt9_sprcom-cms-frontend_apress.css?r=201702071421-9" media="screen" /><![endif]-->
<link rel="stylesheet" type="text/css" href="//static.springer.com/spcom/min/print.css?r=201702071421-9" media="print" />
<link rel="stylesheet" type="text/css" href="/spcom/css/vendor/font-awesome.min.css?r=201702071421-9" />
<!--[if lt IE 9]><script type="text/javascript" id="ielt9js" charset="utf-8" src="//static.springer.com/spcom/min/ielt9.js"></script>
注意

这是在 Java 9 jShell 实用程序中尝试的一个很好的例子。要启动该实用程序,请打开命令提示符或终端,并键入 jshell–add-modules Java . httpclient 来启动包含 http client 模块的 shell。这假设 jshell 可执行实用程序驻留在路径中。

它是如何工作的

Java 9 中添加了新的 javax.httpclient 模块,它由高级 HTTP 和 WebSocket 客户端 API 组成。API 为 HTTP 客户端提供了同步和异步实现,为 WebSocket 提供了异步实现。API 驻留在 java.net.http 包中。

HttpClient 是 HttpRequests 类型通用的配置信息容器。HttpClient 是通过启动 HttpRequest 生成的。生成器,为请求传递 URI,然后调用 create()方法。create()方法返回一个不可变的 HttpClient。正如在这个配方的解决方案中所看到的,客户端可以用来执行许多活动,包括同步的和异步的。异步请求将返回一个 CompleteableFuture 对象以供使用。有关 HTTP 和 WebSocket 客户端的更多详细信息,请参考配方 21-7 和配方 21-8。

2-7.重定向平台日志

问题

如果日志符合指定的过滤标准,您希望过滤日志并将其重定向到特定文件。

解决办法

利用 Java 9 统一日志 API 来过滤日志并相应地进行路由。在下面的命令行或终端摘录中,调用 java 可执行文件来执行名为 Recipe02_07 的类。指定了-Xlog 选项,传递 gc=debug:file=gc.txt:none。这表示所有使用“调试”级别标记为“gc”的日志消息都应该写入文件 gc.txt,并且不应该使用任何修饰。

java -Xlog:gc=debug:file=gc.txt:none Recipe02_07

这将导致在当前目录中创建一个名为 gc.txt 的文件,所有与指定的–Xlog 选项相关的日志消息都将被写入该文件。

它是如何工作的

JVM 是一个复杂的系统,有时很难确定问题的原因。Java 9 中添加的统一 JVM 日志系统提供了一个更细粒度的解决方案,有助于找到问题的原因。新的命令行选项–Xlog 控制这一功能,提供了许多标签、级别、装饰和输出选项来实现卓越的日志记录。标签可以通过名字来指定,比如 gc、threads、compiler 等等。可能有不同的日志记录级别,包括错误、警告、信息、调试、跟踪和开发。如果指定了“off”级别,则日志记录将被禁用。可以指定装饰者来提供关于消息的详细信息。装饰者可以以定制的顺序来指定,这样期望的结果将被记录。表 2-2 包含了可以使用的不同装饰器的列表。

表 2-2。-Xlog 装饰工
|

装饰者

|

功能

|
| --- | --- |
| 时间 | 以 ISO-8601 格式提供当前时间和日期。 |
| 正常运行时间 | JVM 启动并运行的时间(秒和毫秒)。 |
| 时间英里 | 从 System.currentTimeInMillis()返回的值; |
| uptimemillis | JVM 的正常运行时间(毫秒)。 |
| 时间纳米 | 从 System.nanoTime()返回的值; |
| 上升趋势 | JVM 的正常运行时间,单位为纳秒。 |
| pid | 进程标识符。 |
| 每日三次 | 线程标识符。 |
| 水平 | 日志消息级别。 |
| 标签 | 与日志消息关联的标记集。 |

支持三种类型的输出,即:stdout、stderr 和文本文件。输出文件的旋转配置是可能的。当所有这些选项一起指定时,日志记录会变得非常详细。有关可能选项的完整列表,请参考 http://openjdk.java.net/jeps/158 的 JEP()。

2-8.利用工厂方法创建不可变集合

问题

您希望生成一个不可变的值集合。

解决办法

利用 Collection.of()构造生成一个不可变的集合。在下面的示例中,创建了两个集合。第一个是不可变列表,第二个是不可变映射

List<String> jvmLanguages = List.of("Java", "Scala", "JRuby", "Groovy", "Jython", "Kotlin");
System.out.println(jvmLanguages);
try {
    jvmLanguages.add("Exception");
} catch (UnsupportedOperationException uoe){
    System.out.println(uoe);
}
Map <Integer, String> players = Map.of(1, "Josh Juneau", 2, "Jonathan Gennick", 3, "Freddy Guime", 4, "Carl Dea");
System.out.println(players.values());
System.out.println("Player 2: " + players.get(2));

输出如下所示。请注意,在示例中,我添加了一个 try-catch 块来捕获当我试图修改列表时抛出的 UnsupportedOperationException。

[Java, Scala, JRuby, Groovy, Jython, Kotlin]
java.lang.UnsupportedOperationException
[Carl Dea, Jonathan Gennick, Freddy Guime, Josh Juneau]
Player 2: Jonathan Gennick

它是如何工作的

历史上,Java 是一种执行小任务的冗长语言。在过去,构建一个填充的数据集合需要几行代码。在第一行,集合必须被初始化,然后是添加到其中的每一项的一行代码。Java 9 增加了方便的 API,可以快速生成不可修改的数据集合,这样就可以在一行代码中初始化和填充构造。

工厂方法已经被添加到 List、Set 和 Map 接口中,用于创建这种不可修改的数据集合。工厂方法由 of()方法组成,该方法接受多达 10 个值,用于快速创建不可变的集合。映射工厂方法最多接受十个键/值对。如果 Map 需要十个以上的对,那么可以调用 Map.ofEntries()方法,传递任意数量的 Map。条目。此外,不能使用空值来填充元素、键或值。

摘要

本章介绍了 Java 9 中添加的一些新特性和增强功能。虽然肯定不是新特性的完整列表,但本章深入研究了一些最值得期待的特性,包括模块化、流程 API 和简单的错误处理。为了更全面地了解新特性,应该通读整本书。然而,这一章让你对将要发生的事情有所了解。

三、字符串

字符串是任何编程语言中最常用的数据类型之一。它们可以用来从键盘上获取文本,将消息打印到命令行,等等。鉴于字符串的使用如此频繁,随着时间的推移,String 对象中增加了许多特性,以使它们更易于使用。毕竟,字符串是 Java 中的一个对象,所以它包含可用于操作字符串内容的方法。在 Java 中,字符串也是不可变的,这意味着它们的状态不能被改变。这使得它们与一些可变的数据类型有所不同。理解如何正确利用不可变对象是很重要的,尤其是当试图改变或给它们赋不同的值时。

本章重点介绍一些最常用的处理字符串对象的方法和技术。我们还将介绍一些有用的技术,这些技术并不是字符串对象所固有的。

压缩字符串:Java 9 字符串增强

自从引入 Java 语言以来,字符串就被存储在 UTF-16 char 类型的数组中。char 数组为每个字符包含两个字节,这最终会产生一个很大的内存堆,因为字符串在我们的应用中经常使用。在 Java 9 中,字符串存储在 byte 类型的数组中,存储的字符编码为 ISO-8859-1/Latin-1(每个字符一个字节)或 UTF-16(每个字符两个字节)。char 数组上还有一个编码标志,用于指示字符串使用的编码类型。这些变化也被称为紧凑字符串

这些变化不会影响我们使用字符串的方式,也不会以任何方式改变 String 类的 helper 方法。但是,它们可能会显著减少应用使用的内存量。

3-1.获取字符串的子部分

问题

你想检索一个字符串的一部分。

解决办法

使用 substring()方法获取两个不同位置之间的字符串部分。在下面的解决方案中,创建了一个字符串,然后使用 substring()方法打印出字符串的各个部分。

public static void substringExample(){
    String originalString = "This is the original String";
        System.out.println(originalString.substring(0, originalString.length()));
        System.out.println(originalString.substring(5, 20));
        System.out.println(originalString.substring(12));
    }

运行此方法将产生以下结果:

This is the original String
is the original
original String

它是如何工作的

String 对象包含许多 helper 方法。substring()就是这样一种方法,它可用于返回字符串的一部分。substring()方法有两种变体。其中一个接受单一参数,即起始索引;另一个接受两个参数:startingindex 和 endingindex。substring()方法有两种变体,这使得第二个参数看起来是可选的;如果未指定,则使用调用字符串的长度来代替它。应该注意,索引从零开始,因此字符串中的第一个位置的索引为 0,依此类推。

从这个配方的解决方案中可以看出,第一次使用 substring()打印出了字符串的全部内容。这是因为传递给 substring()方法的第一个参数是 0,传递的第二个参数是原始字符串的长度。在 substring()的第二个示例中,索引 5 用作第一个参数,索引 20 用作第二个参数。这实际上导致只返回字符串的一部分,从字符串中位于第六个位置的字符开始,或索引 5,因为第一个位置的索引为 0;并以字符串中位于第 20 个位置的字符(索引为 19)结束。第三个示例只指定了一个参数;因此,结果将是从该参数指定的位置开始的原始字符串。

注意

substring()方法只接受正整数值。如果试图传递负值,将会引发异常。

3-2.比较字符串

问题

您正在编写的应用需要能够比较两个或多个字符串值。

解决办法

使用内置的 equals()、equalsIgnoreCase()、compareTo()和 compareToIgnoreCase()方法来比较字符串中包含的值。下面是使用不同字符串比较操作的一系列测试。

如您所见,如果比较结果相等,则使用各种 if 语句来打印消息:

String one = "one";
String two = "two";

String var1 = "one";
String var2 = "Two";

String pieceone = "o";
String piecetwo = "ne";

// Comparison is equal
if (one.equals(var1)){
    System.out.println ("String one equals var1 using equals");
}

// Comparison is NOT equal
if (one.equals(two)){
    System.out.println ("String one equals two using equals");
}

// Comparison is NOT equal
if (two.equals(var2)){
    System.out.println ("String two equals var2 using equals");
}

// Comparison is equal, but is not directly comparing String values using ==
if (one == var1){
    System.out.println ("String one equals var1 using ==");
}

// Comparison is equal
if (two.equalsIgnoreCase(var2)){
    System.out.println ("String two equals var2 using equalsIgnoreCase");
}

System.out.println("Trying to use == on Strings that are pieced together");

String piecedTogether = pieceone + piecetwo;

// Comparison is equal
if (one.equals(piecedTogether)){
    System.out.println("The Strings contain the same value using equals");
}

// Comparison is NOT equal using ==
if (one == piecedTogether) {
    System.out.println("The String contain the same value using == ");
}

// Comparison is equal
if (one.compareTo(var1) == 0){
    System.out.println("One is equal to var1 using compareTo()");
}

产生以下输出:

String one equals var1 using equals
String one equals var1 using ==
String two equals var2 using equalsIgnoreCase
Trying to use == on Strings that are pieced together
The Strings contain the same value using equals
One is equal to var1 using compareTo()

它是如何工作的

当试图比较两个或更多的值,尤其是字符串值时,使用编程语言会遇到一个更棘手的问题。在 Java 语言中,比较字符串非常简单,记住应该而不是使用进行字符串比较。这是因为比较运算符()用于比较引用,而不是字符串的值。在用 Java 进行字符串编程时,最吸引人的事情之一是使用比较运算符,但您不能这样做,因为结果可能会有所不同。

注意

Java 使用字符串的内部化来提高性能。这意味着 JVM 包含一个包含被拘留字符串的表,每次在字符串上调用 intern()方法时,都会在该表上执行查找以找到匹配。实习返回字符串的规范表示。如果表中没有匹配的字符串,则将该字符串添加到表中并返回一个引用。如果字符串已经存在于表中,则返回引用。Java 会自动填充字符串文字,当使用==比较运算符时,这会导致变化。

在这个配方的解决方案中,您可以看到各种不同的比较字符串值的技术。equals()方法是每个 Java 对象的一部分。Java String equals()方法已被覆盖,因此它将比较字符串中包含的值,而不是对象本身。从下面的例子中可以看出,equals()方法是一种比较字符串的安全方法。

// Comparison is equal
if (one.equals(var1)){
    System.out.println ("String one equals var1 using equals");
}
// Comparison is NOT equal
if (one.equals(two)){
    System.out.println ("String one equals two using equals");
}

equals()方法将首先使用==操作符检查字符串是否引用同一个对象;如果它们这样做,它将返回 true。如果它们不引用同一个对象,equals()将逐个字符地比较每个字符串,以确定相互比较的字符串是否包含完全相同的值。如果其中一个字符串与另一个字符串的大小写设置不同,该怎么办?他们还用 equals()互相比较相等吗?答案是否定的,这就是创建 equalsIgnoreCase()方法的原因。使用 equalsIgnoreCase()比较两个值会导致比较每个字符时不注意大小写。以下示例摘自该配方的解决方案:

// Comparison is NOT equal
if (two.equals(var2)){
    System.out.println ("String two equals var2 using equals");
}
// Comparison is equal
if (two.equalsIgnoreCase(var2)){
    System.out.println ("String two equals var2 using equalsIgnoreCase");
}

compareTo()和 compareToIgnoreCase()方法执行字符串的字典式比较。这种比较基于字符串中包含的每个字符的 Unicode 值。如果字符串在字典顺序上位于参数字符串之前,则结果将是负整数。如果字符串按字典顺序跟在参数字符串后面,则结果将是正整数。如果两个字符串在字典上彼此相等,结果将为零。以下摘录自该配方的解决方案,演示了 compareTo()方法:

// Comparison is equal
if (one.compareTo(var1) == 0){
    System.out.println("One is equal to var1 using compareTo()");
}

不可避免地,许多应用包含必须在某种程度上比较字符串的代码。下一次当您有一个需要字符串比较的应用时,在编写代码之前,请考虑一下本食谱中讨论的信息。

3-3.修剪空白

问题

您正在处理的一个字符串的两端包含一些空格。你想要去掉那些空白。

解决办法

使用 String trim()方法消除空白。在下面的例子中,打印的句子两边都有空格。然后,使用 trim()方法再次打印同一个句子,删除空白,以便可以看到更改。

String myString = " This is a String that contains whitespace.   ";
System.out.println(myString);
System.out.println(myString.trim());

输出将如下所示:

 This is a String that contains whitespace.
This is a String that contains whitespace.

它是如何工作的

无论我们多么小心,在处理文本字符串时,空白总是会成为一个问题。将字符串与匹配值进行比较时尤其如此。如果一个字符串包含一个意外的空白字符,那么这对于模式搜索程序来说可能是灾难性的。幸运的是,Java String 对象包含 trim()方法,可以用来自动删除任何给定字符串末尾的空格。

trim()方法非常容易使用。事实上,正如您可以从这个配方的解决方案中看到的,使用 trim()方法所需要的只是对任何给定字符串的调用。因为字符串是对象,所以它们包含许多帮助器方法,这使得它们非常容易使用。毕竟,字符串是任何编程语言中最常用的数据类型之一…所以它们最好易于使用!trim()方法返回原始字符串的副本,去掉了所有的前导和尾随空格。但是,如果没有要删除的空格,trim()方法将返回原始的字符串实例。没有比这更简单的了!

3-4.更改字符串的大小写

问题

应用的一部分包含区分大小写的字符串值。您希望在处理之前将所有字符串都改为大写,以避免以后出现任何区分大小写的问题。

解决办法

使用 toUpperCase()和 toLowerCase()方法。String 对象提供了这两个帮助器方法来帮助对给定字符串中的所有字符执行大小写更改。

例如,给定下面代码中的字符串,将调用这两个方法中的每一个:

String str = "This String will change case.";
System.out.println(str.toUpperCase());
System.out.println(str.toLowerCase());

将产生以下输出:

THIS STRING WILL CHANGE CASE.
this String will change case.

它是如何工作的

要确保给定字符串中每个字符的大小写都是大写或小写,请分别使用 toUpperCase()和 toLowerCase()方法。使用这些方法时,有几个事项需要注意。首先,如果给定的字符串包含一个大写字母,并且对其调用 toUpperCase()方法,则大写字母将被忽略。同样的概念也适用于调用 toLowerCase()方法。给定字符串中包含的任何标点符号或数字也会被忽略。

这些方法中的每一种都有两种变体。其中一个变体不接受任何参数,而另一个变体接受与您希望使用的地区相关的参数。不带任何参数调用这些方法将导致使用默认区域设置的大小写转换。如果要使用不同的区域设置,可以使用接受参数的方法的变体,将所需的区域设置作为参数传递。例如,如果您想使用意大利语或法语地区,您可以使用以下代码:

System.out.println(str.toUpperCase(Locale.ITALIAN));
System.out.println(str.toUpperCase(new Locale("it","US")));
System.out.println(str.toLowerCase(new Locale("fr", "CA")));

使用这些方法将字符串转换成大写或小写可以使事情变得简单。它们对于比较作为应用输入的字符串也非常有用。考虑这样一种情况,用户被提示输入用户名,结果被保存到字符串中。现在考虑在程序的后面,将该字符串与数据库中存储的所有用户名进行比较,以确保用户名有效。如果输入用户名的人用大写的第一个字符键入它,会发生什么?如果用户名全部以大写形式存储在数据库中,会发生什么情况?这种比较永远不会相等。在这种情况下,开发人员可以使用 toUpperCase()方法来缓解这个问题。对正在比较的字符串调用此方法将导致两个字符串的大小写相同。

3-5.串联字符串

问题

有各种各样的字符串,你想结合成一个。

解决方案 1

如果要将字符串连接到彼此的末尾,请使用 concat()方法。下面的示例演示了 concat()方法的用法:

String one = "Hello";
String two = "Java9";
String result = one.concat(" ".concat(two));

结果是这样的:

Hello Java9

解决方案 2

使用串联运算符以速记方式组合字符串。在以下示例中,在两个字符串之间放置了一个空格字符:

String one = "Hello";
String two = "Java9";
String result = one + " " + two;

结果是这样的:

Hello Java9

解决方案 3

使用 StringBuilder 或 StringBuffer 来组合字符串。下面的示例演示如何使用 StringBuffer 连接两个字符串:

String one = "Hello";
String two = "Java9";
StringBuffer buffer = new StringBuffer();
buffer.append(one).append(" ").append(two);
String result = buffer.toString();
System.out.println(result);

结果是这样的:

Hello Java9

它是如何工作的

Java 语言为连接文本字符串提供了几个不同的选项。虽然没有一个比其他的更好,但是你可能会发现在不同的情况下有一个比另一个更好。concat()方法是一个内置的字符串帮助器方法。它提供了将一个字符串附加到另一个字符串的末尾的能力,如该配方的解决方案 1 所示。concat()方法将接受任何字符串值;因此,如果需要,可以显式键入一个字符串值作为参数传递。如解决方案 1 所示,简单地将一个字符串作为参数传递给该方法会将它附加到字符串的末尾,该方法将被调用。但是,如果您想要在两个字符串之间添加一个空格字符,您可以通过传递一个空格字符以及您想要追加的字符串来实现,如下所示:

String result = one.concat(" ".concat(two));

正如您所看到的,能够向 concat()方法传递任何字符串或字符串组合使它非常有用。因为所有字符串帮助器方法实际上都返回应用了帮助器方法功能的原始字符串的副本,所以您也可以将调用其他帮助器方法的字符串传递给 concat()(或任何其他字符串帮助器方法)。假设您想要显示文本“Hello Java”而不是“Hello Java9”。以下字符串帮助器方法的组合将允许您这样做:

String one = "Hello";
String two = "Java9";
String result = one.concat(" ".concat(two.substring(0, two.length()-1)));

串联运算符(+)可用于组合任意两个字符串。它几乎被认为是 concat()方法的简写形式。本例的解决方案 3 中演示的最后一项技术是使用 StringBuffer,这是一个可变的字符序列,很像字符串,只是它可以通过方法调用来修改。StringBuffer 类包含许多用于构建和操作字符序列的 helper 方法。在解决方案中,append()方法用于追加两个字符串值。append()方法将作为参数传递的字符串放在 StringBuffer 的末尾。有关使用 StringBuffer 的更多信息,请参考位于docs . Oracle . com/javase/9/docs/API/Java/lang/string buffer . html的在线文档。

3-6.将字符串转换为数值

问题

您希望能够将存储为字符串的任何数值转换为整数。

解决方案 1

使用 Integer.valueOf() helper 方法将字符串转换为 int 数据类型。例如:

String one = "1";
String two = "2";
int result = Integer.valueOf(one) + Integer.valueOf(two);

如您所见,这两个字符串变量都被转换为整数值。之后,它们被用于执行加法计算,然后存储到一个 int 中。

注意

本例中使用了一种称为自动装箱的技术。自动装箱是 Java 语言的一个特性,它可以自动将原始值转换成相应的包装类。例如,当您将 int 值赋给一个整数时,就会出现这种情况。类似地,取消装箱会在您尝试从包装类到原语的反方向转换时自动发生。有关自动装箱的更多信息,请参考位于docs . Oracle . com/javase/tutorial/Java/data/autobox ing . html的在线文档。

解决方案 2

使用 Integer.parseInt() helper 方法将字符串转换为 Int 数据类型。例如:

String one = "1";
String two = "2";
int result = Integer.parseInt(one) + Integer.parseInt(two);
System.out.println(result);

它是如何工作的

Integer 类包含 valueOf()和 parseInt()方法,用于将字符串或 Int 类型转换为整数。Integer 类的 valueOf()类型有两种不同的形式,可用于将字符串转换为整数值。每种方法的不同之处在于它们接受的参数数量。第一个 valueOf()方法只接受字符串参数。如果可能的话,该字符串将被解析为一个整数值,然后返回一个包含该字符串值的整数。如果字符串没有正确地转换成整数,那么该方法将抛出 NumberFormatException。

Integer 的 valueOf()方法的第二个版本接受两个参数:一个将被解析为整数的 String 参数和一个表示用于转换的基数的 int 参数。

注意

许多 Java 类型类都包含 valueOf()方法,可用于将不同的类型转换成该类的类型。String 类就是这种情况,因为它包含许多不同的 valueOf()方法,可用于转换。有关字符串类或任何其他类型类包含的不同 valueOf()方法的更多信息,请参见位于docs.oracle.com/javase/9/docs的在线 Java 文档

Integer 类的 parseInt()方法也有两种不同的形式。其中一个接受一个参数:要转换成整数的字符串。另一种形式接受两个参数:要转换成整数的字符串和基数。第一种格式使用最广泛,它将字符串参数解析为有符号的十进制整数。如果字符串中不包含可分析的无符号整数,将引发 NumberFormatException。第二种格式不太常用,它返回一个 Integer 对象,该对象保存由给定基数的字符串参数表示的值,前提是该字符串中包含一个可解析的无符号整数。

注意

parseInt()和 valueOf()最大的区别之一是 parseInt()返回一个 Int,valueOf()从缓存中返回一个整数。

3-7.迭代字符串中的字符

问题

您希望迭代一个文本字符串中的字符,以便可以在字符级别操作它们。

解决办法

使用字符串帮助器方法的组合来获得对字符级字符串的访问。如果在循环上下文中使用 String helper 方法,可以很容易地按字符遍历字符串。在下面的示例中,使用 toCharArray()方法分解名为 str 的字符串。

String str = "Break down into chars";
System.out.println(str);
for (char chr:str.toCharArray()){
    System.out.println(chr);
}

相同的策略可以用于传统版本的 for 循环。可以创建一个索引,允许使用 charAt()方法访问字符串中的每个字符。

for (int x = 0; x <= str.length()-1; x++){
System.out.println(str.charAt(x));
}

这两种解决方案都会产生以下结果:

B
r
e
a
k

d
o
w
n

i
n
t
o

c
h
a
r
s
注意

第一个示例使用 toCharArray()生成一个新的字符数组。因此,使用传统 for 循环的第二个示例可能会执行得更快。

它是如何工作的

String 对象包含可用于执行各种任务的方法。这个配方的解决方案演示了许多不同的字符串方法。可以对字符串调用 toCharArray()方法,以便将字符串分解成字符,然后将这些字符存储在一个数组中。这个方法非常强大,当需要执行这个任务时,它可以节省一点时间。调用 toCharArray()方法的结果是一个 char[],然后可以使用索引遍历它。这个菜谱的解法就是这样。增强的 for 循环用于遍历 char[]的内容,并打印出它的每个元素。

String length()方法用于查找字符串中包含的字符数。结果是一个 int 值,这在 for 循环的上下文中非常有用,如这个配方的解决方案所示。在第二个示例中,length()方法用于查找字符串中的字符数,以便可以使用 charAt()方法迭代这些字符。charAt()方法接受一个 int 索引值作为参数,并返回位于字符串中给定索引处的字符。

通常,两个或多个字符串方法的组合可用于获得各种结果。在这种情况下,在同一个代码块中使用 length()和 charAt()方法提供了将字符串分解成字符的能力。

3-8.查找文本匹配

问题

您希望在文本中搜索特定的字符序列。

解决方案 1

利用正则表达式和 String matches()辅助方法来确定存在多少个匹配。要做到这一点,只需将表示正则表达式的字符串传递给 matches()方法,以匹配任何要匹配的字符串。这样做时,该字符串将与调用()时匹配的字符串进行比较。一旦被评估,matches()将产生一个布尔结果,表明它是否匹配。以下代码摘录包含一系列使用这种技术的示例。代码中包含的注释解释了每个匹配测试。

String str = "Here is a long String...let's find a match!";
// This will result in a "true" since it is an exact match
boolean result = str.matches("Here is a long String...let's find a match!");
System.out.println(result);
// This will result iin "false" since the entire String does not match
result = str.matches("Here is a long String...");

System.out.println(result);

str = "true";

// This will test against both upper & lower case "T"...this will be TRUE
result = str.matches("[Tt]rue");
System.out.println(result);

// This will test for one or the other
result = str.matches("[Tt]rue|[Ff]alse]");
System.out.println(result);

// This will test to see if any numbers are present, in this case the
// person writing this String would be able to like any Java release!
str = "I love Java 8!";
result = str.matches("I love Java [0-9]!");
System.out.println(result);

// This will test TRUE as well...
str = "I love Java 7!";
result = str.matches("I love Java [0-9]!");
System.out.println(result);

// The following will test TRUE for any language that contains
// only one word for a name. This is because it tests for
// any alphanumeric combination. Notice the space character
// between the numeric sequence...
result = str.matches("I love .*[ 0-9]!");
System.out.println(result);

// The following String also matches.
str = "I love Jython 2.5.4!";
result = str.matches("I love .*[ 0-9]!");

System.out.println(result);

示例中输出的每个结果都是真的,只有第二个示例例外,因为它不匹配。

解决方案 2

与 String matches()方法相比,使用正则表达式模式和 Matcher 类可以获得性能更好、功能更丰富的匹配解决方案。尽管 matches()方法在大多数情况下可以完成工作,但是在某些情况下,您需要一种更灵活的匹配方式。使用此解决方案需要三个步骤:

  1. 将模式编译成模式对象。

  2. 使用模式上的 matcher()方法构造一个 Matcher 对象。

  3. 在匹配器上调用 matches()方法。

在下面的示例代码中,演示了模式和匹配器技术:

String str = "I love Java 9!";
boolean result = false;

Pattern pattern = Pattern.compile("I love .*[ 0-9]!");
Matcher matcher = pattern.matcher(str);
result = matcher.matches();

System.out.println(result);

前面的例子将产生一个真值,就像解决方案 1 中演示的它的变体一样。

它是如何工作的

正则表达式是查找匹配的好方法,因为它们允许定义模式,这样应用就不必显式地查找精确的字符串匹配。当您想要查找用户输入到程序中的文本的匹配时,它们会非常有用。但是,如果您试图将字符串与您在程序中定义的字符串常量进行匹配,那么它们可能是多余的,因为 String 类提供了许多可用于此类任务的方法。然而,在几乎每个开发人员的生活中,肯定会有正则表达式派上用场的时候。它们几乎可以在今天使用的每一种编程语言中找到。Java 使它们易于使用和理解。

注意

尽管如今正则表达式在许多不同的语言中使用,但每种语言的表达式语法各不相同。有关正则表达式语法的完整信息,请参见位于docs . Oracle . com/javase/9/docs/API/Java/util/regex/pattern . html的在线文档。

使用正则表达式最简单的方法是在 String 对象上调用 matches()方法。将一个正则表达式传递给 matches()方法将产生一个布尔结果,表明该字符串是否匹配给定的正则表达式模式。此时,了解什么是正则表达式以及它是如何工作的是很有用的。

一个正则表达式是一个字符串模式,可以与其他字符串进行匹配,以确定其内容。正则表达式可以包含许多不同的模式,这使它们能够是动态的,因为它们能够匹配许多包含相同格式的不同字符串。例如,在这个配方的解决方案中,下面的代码可以匹配几个不同的字符串:

result = str.matches("I love Java [0-9]!");

这个例子中的正则表达式字符串是“我爱 Java [0-9]!”,它包含模式[0-9],表示 0 到 9 之间的任何数字。因此,任何显示“I love Java”后跟数字 0 到 9 以及一个感叹号的字符串都将匹配正则表达式字符串。要查看可以在正则表达式中使用的所有不同模式的列表,请参阅上一节中 URL 处的在线文档。

Pattern 和 Matcher 对象的组合也可以用来获得与 String matcher()方法类似的结果。Pattern 对象可用于将字符串编译成正则表达式模式。如果一个编译过的模式被多次使用,它可以为应用提供性能增益。您可以将基于字符串的正则表达式传递给 Pattern.compile()方法,就像传递给 String matches()方法一样。结果是一个编译后的模式对象,可以与字符串进行匹配以进行比较。通过对给定的字符串调用模式对象的 matcher()方法,可以获得 Matcher 对象。一旦获得了 Matcher 对象,就可以使用下面三种方法中的任何一种来将给定的字符串与模式进行匹配,这三种方法都返回一个表示匹配的布尔值。解决方案 2 的以下三行代码可以作为使用 Pattern.matches()方法的替代解决方案,但不考虑编译模式的可重用性:

Pattern pattern = Pattern.compile("I love .*[ 0-9]!");
Matcher matcher = pattern.matcher(str);
result = matcher.matches();
  • Matcher matches()方法尝试将整个输入字符串与模式进行匹配。

  • Matcher lookingAt()方法试图将输入字符串与开头的模式进行匹配。

  • Matcher find()方法扫描输入序列,寻找字符串中的下一个匹配序列。

在这个配方的解决方案中,对 Matcher 对象调用 matches()方法,以便尝试匹配整个字符串。在任何情况下,正则表达式对于匹配字符串和模式都非常有用。使用正则表达式的技术在不同的情况下会有所不同,使用哪种方法最适合这种情况。

3-9.替换所有文本匹配

问题

您已经在文本正文中搜索了特定的字符序列,并且希望用另一个字符串值替换所有匹配项。

解决办法

使用正则表达式模式获得匹配器对象;然后使用 Matcher 对象的 replaceAll()方法将所有匹配替换为另一个字符串值。下面的示例演示了这种技术:

String str = "I love Java 8!  It is my favorite language.  Java 8
 is the "
                + "8th version of this great programming language.";
Pattern pattern = Pattern.compile("[0-9]");
Matcher matcher = pattern.matcher(str);
System.out.println("Original: " + str);
System.out.println(matcher.matches());
System.out.println("Replacement: " + matcher.replaceAll("9"));

该示例将产生以下结果:

Original: I love Java 8! It is my favorite language. Java 8 is the 8th version of this great programming language.
Replacement: I love Java 9! It is my favorite language. Java 9 is the 9th version of this great programming language.

它是如何工作的

Matcher 对象的 replaceAll()方法使得查找和替换包含在文本主体中的字符串或字符串的一部分变得容易。为了使用 Matcher 对象的 replaceAll()方法,必须首先通过将正则表达式字符串模式传递给 Pattern.compile()方法来编译模式对象。通过调用 matcher()方法,使用产生的 Pattern 对象获得 Matcher 对象。下面几行代码显示了这是如何实现的:

Pattern pattern = Pattern.compile("[0-9]");
Matcher matcher = pattern.matcher(str);

获得 Matcher 对象后,调用它的 replaceAll()方法,传递一个字符串来替换所有与编译模式匹配的文本。在这个配方的解决方案中,字符串“9”被传递给 replaceAll()方法,因此它将替换字符串中匹配“[0-9]”模式的所有区域。

3-10.确定文件后缀是否匹配给定的字符串

问题

您正在从服务器读取一个文件,您需要确定它是什么类型的文件,以便正确地读取它。

解决办法

通过对给定的文件名使用 endsWith()方法来确定文件的后缀。在下面的示例中,假设变量 filename 包含给定文件的名称,并且代码使用 endsWith()方法来确定 filename 是否以特定字符串结尾:

if(filename.endsWith(".txt")){
    System.out.println("Text file");
} else if (filename.endsWith(".doc")){
    System.out.println("Document file");
} else if (filename.endsWith(".xls")){
    System.out.println("Excel file");
} else if (filename.endsWith(".java")){
System.out.println("Java source file");
} else {
    System.out.println("Other type of file");
}

假设文件名变量中包含文件名及其后缀,这段代码将读取其后缀,并确定给定变量代表的文件类型。

它是如何工作的

如前所述,String 对象包含许多可用于执行任务的助手方法。String 对象的 endsWith()方法接受一个字符序列,然后返回一个布尔值,该值表示原始字符串是否以给定的序列结尾。对于这个配方的解决方案,在 if 块中使用了 endsWith()方法。一系列文件后缀被传递给 endsWith()方法,以确定 filename 变量所代表的文件类型。如果任何文件名后缀匹配,则打印一行,说明文件的类型。

3-11.制作可以包含动态信息的字符串

问题

您希望生成一个能够包含动态占位符的字符串,以便该字符串可以根据应用数据的变化而变化。

解决方案 1

利用 String format()内置方法生成包含动态数据占位符的字符串。下面的示例演示了一个包含动态占位符的字符串,该占位符允许将不同的数据插入到同一字符串中。在本例中,随着温度变量的变化,字符串也会动态改变。

public static void main(String[] args){
    double temperature = 98.6;
    String temperatureString = "The current temperature is %.1f degrees Farenheit.";

    System.out.println(String.format(temperatureString, temperature));

    temperature = 101.2;

    System.out.println(String.format(temperatureString, temperature));
}

输出:

The current temperature is 98.6 degrees Farenheit.
The current temperature is 101.2 degrees Farenheit.

解决方案 2

如果希望将字符串的内容打印出来,而不是存储起来供以后使用,可以使用 System.out.printf()方法在字符串中定位动态值。下面的示例演示了与解决方案 1 中相同的概念,只是这次没有使用 String.format()方法,而是简单地打印出一个字符串,并且在运行时用动态内容替换传递给 System.out.printf()方法的占位符。

public static void main(String[] args){
    double temperature = 98.6;
    System.out.printf("The current temperature is %.1f degrees Farenheit.\n", temperature);

    temperature = 101.2;
    System.out.printf("The current temperature is %.1f degrees Farenheit.", temperature);    
}

输出:

The current temperature is 98.6 degrees Farenheit.
The current temperature is 101.2 degrees Farenheit.

它是如何工作的

当您需要使用动态字符串内容时,format()实用程序可以派上用场。format()内置方法允许在字符串中放置占位符,这样占位符将在运行时被动态内容替换。format 方法接受一个字符串和一系列变量,这些变量将用于在运行时用动态内容替换字符串中的占位符。占位符必须专门指定用于替换它们的内容类型。表 3-1 包含 String.format()函数的每个占位符或转换类型的列表。

表 3-1。String.format()转换类型
|

转换

|

内容类型

|
| --- | --- |
| b | 布尔 |
| h | 十六进制 |
| s | 线 |
| c | Unicode 字符 |
| d | 十进制整数 |
| o | 八进制整数 |
| x | 十六进制整数 |
| e | 计算机科学记数法中的浮点十进制数 |
| f | 浮点十进位数字 |
| g | 根据舍入后的精度和值,使用计算机化的科学记数法或十进制格式的浮点 |
| a | 带有效数字和指数的十六进制浮点数 |
| t | 日期/时间 |
| n | 特定于平台的行分隔符 |

每个占位符必须以%字符开头,以表示它是字符串中的占位符。占位符还可以包含标志、宽度和精度指示器,以帮助适当地格式化动态值。应该使用以下格式来构建每个占位符:

%[flags][width][.precision]conversion_indicator

第二个解决方案演示了如何利用 System.out.printf()方法,该方法接受与 System.format()方法相同的参数。这两者的主要区别在于 System.out.printf()方法对于打印格式化内容来说非常方便。如果您的应用需要存储格式化的值,那么您更有可能使用 String.format()方法。

摘要

本章讲述了使用字符串的基础知识。虽然字符串可能看起来像一个简单的字符串,但它是一个包含许多方法的对象,这些方法对于获得所需的结果非常有用。尽管字符串是不可变的对象,但是 String 类中的许多方法都包含字符串的副本,可以根据请求进行修改。本章介绍了这些方法中的一小部分,展示了一些特性,比如连接、如何获得字符串的一部分、修剪空白和替换字符串的一部分。

四、数字和日期

数字在许多应用中扮演着重要的角色。因此,了解如何在您试图执行的工作环境中正确使用它们是很有帮助的。本章帮助您了解如何使用数字执行一些最基本的操作,并且还提供了关于执行高级任务(如使用货币)的见解。使用货币有很多方法,本章将重点介绍其中的几种。

日期也很重要,因为它们在应用中有多种用途。在 Java 8 中,引入了名为 java.time 的新日期时间包。日期-时间 API 使用 ISO-8601 中定义的日历作为默认日历。因此,日历是基于公历系统的,在本章中,您将学习如何处理日期、时间和时区数据。日期-时间 API 遵循几个设计原则,因为它清晰、流畅、不可变和可扩展。这个 API 使用了一种清晰的语言,简洁且定义明确。它也非常流畅,因此处理日期-时间数据的代码易于阅读和理解。日期-时间 API 中的大多数类都是不可变的,因此为了改变日期-时间对象,您必须创建原始对象的修改副本。因此,date-time 类中的许多方法都有相应的名称,例如 of()和 with(),这样您就知道您是在创建一个副本,而不是修改原始文件。最后,新的日期-时间 API 可以在许多情况下进行扩展,这使得它在许多环境中都很有用。

日期-时间 API 由一组丰富的类组成,提供了在以前的 API 中很难实现的解决方案。尽管有许多不同的类,但大多数都包含一组相似的方法,因此相同的原则可以应用于所有的日期和时间单位。表 4-1 列出了大多数日期-时间类中常见的一组方法。

表 4-1。日期时间 API 的常用方法
|

方法

|

描述

|
| --- | --- |
| 在 | 将一个对象与另一个对象合并。 |
| 格式 | 将指定的格式应用于临时对象,生成一个字符串。 |
| 从 | 将输入参数转换为目标类的实例。 |
| 得到 | 返回目标对象的部分状态。 |
| 存在 | 查询目标对象。 |
| 负的 | 返回减去指定时间后的目标对象的修改副本。 |
| 关于 | 使用指定的验证输入参数创建一个实例。 |
| 从语法上分析 | 分析输入字符串以产生目标类的实例。 |
| 加 | 返回添加了指定时间量的目标对象的修改副本。 |
| 到 | 将对象转换为不同的类型。 |
| 随着 | 返回指定元素已更改的目标对象的修改副本(相当于 setter)。 |

如前所述,日期-时间 API 是流畅的;因此,它的每个类都位于一个明确标记的包中。表 4-2 列出了组成日期时间 API 的包,以及在每个包中可以找到的类的简要描述。

表 4-2。日期时间 API 包
|

包裹

|

描述

|
| --- | --- |
| java.time | API 的核心类。这些类用于处理基于 ISO-8601 标准的日期时间数据。这些类是不可变的和线程安全的。 |
| java.time.chrono | 使用除 ISO-8601 以外的日历系统的 API。 |
| java.time.format | 用于格式化日期时间数据的类。 |
| java.time.temporal | 允许在日期-时间类之间插值的扩展 API。 |
| java.time.zone | 支持时区数据的类。 |

本章简要概述了一些常用的日期时间功能。如果您将执行与日期和时间相关的重要工作,那么除了本章之外,您还应该阅读在线提供的日期-时间 API 文档。

4-1.将浮点和双精度值舍入为整数

问题

您需要能够将应用中的浮点数或双精度数舍入为整数值。

解决办法

使用 java.lang.Math round()方法之一将数字舍入为所需的格式。Math 类有两种方法可用于对浮点数或双精度值进行舍入。下面的代码演示了如何使用这些方法:

public static int roundFloatToInt(float myFloat){
    return Math.round(myFloat);
}

public static long roundDoubleToLong(double myDouble){
    return Math.round(myDouble);
}

第一个方法 roundFloatToInt()接受一个浮点数,并使用 java.lang.Math 类将该数四舍五入为整数。第二个方法 roundDoubleToLong()接受一个 Double 值,并使用 java.lang.Math 类将该 Double 值四舍五入为 Long 值。

它是如何工作的

java.lang.Math 类包含大量帮助方法,使我们在处理数字时更加轻松。round()方法也不例外,因为它们可以很容易地用于舍入浮点或双精度值。java.lang.Math round()方法的一个版本接受浮点数作为参数。它会将浮点数舍入到最接近的 int 值,并向上舍入。如果参数不是数字(NaN),则返回零。当正无穷大或负无穷大的参数传递给 round()时,结果等于 Integer 的值。MAX_VALUE 或整数。将分别返回 MIN_VALUE。java.lang.Math round()方法的第二个版本接受双精度值。double 值被舍入到最接近的 long 值,并向上舍入。就像另一轮()一样,如果参数是 NaN,将返回一个零。类似地,当正无穷大或负无穷大的参数传递给 round()时,结果等于 Long 的值。MAX_VALUE 或 Long。将分别返回 MIN_VALUE。

注意

NaN、POSITIVE_INFINITY 和 NEGATIVE_INFINITY 是在 Float 和 Double 类中定义的常量值。NaN(非数字)是一个未定义或不可表示的值。例如,NaN 值可以通过将 0.0f 除以 0.0f 来生成。由 POSITIVE_INFINITY 和 NEGATIVE_INFINITY 表示的值是指由生成特定类型(浮点型或双精度型)的极大值或负值的运算生成的值,这些值无法正常表示。例如,1.0/0.0 或–1.0/0.0 会产生这样的值。

4-2.格式化双精度和长十进制值

问题

您需要能够在应用中格式化双精度和长数字。

解决办法

使用 DecimalFormat 类将值格式化并舍入到应用要求的精度。在下面的方法中,接受双精度值并打印格式化的字符串值:

public static void formatDouble(double myDouble){
    NumberFormat numberFormatter = new DecimalFormat("##.000");
    String result = numberFormatter.format(myDouble);
    System.out.println(result);
}

例如,如果传递给 formatDouble()方法的 double 值是 345.9372,则结果如下:

345.937

同样,如果将值. 7697 传递给该方法,结果如下:

.770

每个结果都使用指定的模式进行格式化,然后进行相应的舍入。

它是如何工作的

DecimalFormat 类可以与 NumberFormat 类一起使用,对 double 或 long 值进行舍入和/或格式化。NumberFormat 是一个抽象类,它提供了格式化和解析数字的接口。这个类提供了为每个地区格式化和解析数字的能力,并获得货币、百分比、整数和数字的格式。NumberFormat 类本身非常有用,因为它包含可用于获取格式化数字的工厂方法。事实上,要获得一个格式化的字符串,几乎不需要做什么工作。例如,下面的代码演示了对 NumberFormat 类调用一些工厂方法:

// Obtains an instance of NumberFormat class
NumberFormat format = NumberFormat.getInstance();

// Format a double value for the current locale
String result = format.format(83.404);
System.out.println(result);

// Format a double value for an Italian locale
result = format.getInstance(Locale.ITALIAN).format(83.404);
System.out.println(result);

// Parse a String into a Number
try {
    Number num = format.parse("75.736");
    System.out.println(num);
} catch (java.text.ParseException ex){
    System.out.println(ex);
}

若要使用模式进行格式化,DecimalFormat 类可以与 NumberFormat 一起使用。在这个配方的解决方案中,您看到了通过向其构造函数传递一个模式来创建一个新的 DecimalFormat 实例将返回一个 NumberFormat 类型。这是因为 DecimalFormat 扩展了 NumberFormat 类。因为 NumberFormat 类是抽象的,所以 DecimalFormat 包含了 NumberFormat 的所有功能,以及处理模式的附加功能。因此,就像您在前面的演示中看到的那样,它可以用于处理不同地区的不同格式。这在处理双精度或长格式时提供了极大的灵活性。

如前所述,DecimalFormat 类可以在其构造函数中采用基于字符串的模式。还可以使用 applyPattern()方法在事后将模式应用于 Format 对象。每个模式都包含前缀、数字部分和后缀,这允许您将特定的十进制值格式化为所需的精度,并根据需要包含前导数字和逗号。用于构建模式的符号显示在表 4-3 中。每个模式还包含一个积极和消极的子模式。这两个子模式由分号(;)并且负子模式是可选的。如果不存在负子模式,则使用局部减号。例如,一个完整的模式示例应该是###,# # 0.00;(###,##0.00).

表 4-3。十进制格式模式字符
|

性格;角色;字母

|

描述

|
| --- | --- |
| # | 数字;如果没有数字,则为空白 |
| Zero | 数字;如果没有数字,则为零 |
| 。 | 小数 |
| - | 减号或负号 |
| , | 逗号或分组分隔符 |
| E | 科学符号分隔符 |
| ; | 正负子模式分隔符 |

DecimalFormat 类提供了足够的灵活性,几乎可以在任何情况下格式化 double 和 long 值。

4-3.比较 int 值

问题

你需要比较两个或更多的 int 值。

解决方案 1

使用比较运算符比较整数值。在下面的示例中,三个 int 值相互比较,演示了各种比较运算符:

int int1 = 1;
int int2 = 10;
int int3 = -5;

System.out.println(int1 == int2);  // Result:  false
System.out.println(int3 == int1);  // Result:  false
System.out.println(int1 == int1);  // Result:  true
System.out.println(int1 > int3);   // Result:  true
System.out.println(int2 < int3);   // Result:  false

如您所见,比较运算符将生成一个布尔结果。

解决方案 2

使用 Integer.compare(int,int)方法对两个 int 值进行数值比较。下面几行可以比较第一个解决方案中声明的相同 int 值:

System.out.println("Compare method -> int3 and int1: " + Integer.compare(int3, int1));
// Result -1
System.out.println("Compare method -> int2 and int1: " + Integer.compare(int2, int1));
// Result 1

它是如何工作的

也许最常用的数值比较是针对两个或更多的 int 值。Java 语言使得使用比较运算符来比较一个 int 变得非常容易(见表 4-4 )。

表 4-4。比较运算符
|

操作员

|

功能

|
| --- | --- |
| == | 等于 |
| != | 不等于 |
| > | 大于 |
| < | 不到 |
| >= | 大于或等于 |
| <= | 小于或等于 |

这个方法的第二个解决方案演示了在 Java 7 语言中添加的 integer compare()方法。这个静态方法接受两个 int 值并对它们进行比较,如果第一个 int 值大于第二个,则返回 1,如果两个 int 值相等,则返回 0,如果第一个 int 值小于第二个,则返回-1。若要使用 Integer.compare()方法,请传递两个 int 值,如以下代码所示:

Integer.compare(int3, int1));
Integer.compare(int2, int1));

就像你在学校的数学课一样,这些比较运算符将确定第一个整数是等于、大于还是小于第二个整数。这些比较运算符简单易用,最常见于 if 语句的上下文中。

4-4.比较浮点数

问题

您需要在应用中比较两个或多个浮点值。

解决方案 1

使用 float 对象的 compareTo()方法来执行一个 Float 与另一个 Float 的比较。以下示例显示了 compareTo()方法的实际应用:

Float float1 = new Float("9.675");
Float float2 = new Float("7.3826");
Float float3 = new Float("23467.373");

System.out.println(float1.compareTo(float3));  // Result: -1
System.out.println(float2.compareTo(float3));  // Result: -1
System.out.println(float1.compareTo(float1));  // Result: 0
System.out.println(float3.compareTo(float2));  // Result: 1

调用 compareTo()方法的结果是一个整数值。负结果表示第一个浮点值小于与之比较的浮点值。零表示两个浮点值相等。最后,正的结果表明第一个浮点值大于与之比较的浮点值。

解决方案 2

使用 Float 类 compare()方法来执行比较。下面的示例演示了 Float.compare(Float,float)方法的用法。

System.out.println(Float.compare(float1, float3)); // Result: -1
System.out.println(Float.compare(float2, float3)); // Result: -1
System.out.println(Float.compare(float1, float1)); // Result: 0
System.out.println(Float.compare(float3, float2)); // Result: 1

它是如何工作的

比较两个 float 对象最有用的方法是使用 compareTo()方法。该方法将对给定的浮点对象执行数值比较。结果将是一个整数值,指示第一个 float 在数值上是大于、等于还是小于与之比较的 float。如果浮点值是 NaN,则认为它等于其他 NaN 值或大于所有其他浮点值。此外,浮点值 0.0f 大于浮点值-0.0f。

使用 compareTo()的替代方法是 compare()方法,它也是 Float 类的原生方法。compare()方法是在 Java 1.4 中引入的,它是一个静态方法,以与 compareTo()相同的方式比较两个浮点值。它只是让代码读起来有点不同。compare()方法的格式如下:

Float.compare(primitiveFloat1, primitiveFloat2)

所示的 compare()方法实际上将使用 compareTo()进行以下调用:

new Float(float1).compareTo(new Float(float2));

最后,使用 compareTo()或 compare()将返回相同的结果。

4-5.计算货币价值

问题

您正在开发一个需要使用货币值的应用,但您不确定使用哪种数据类型来存储和计算货币值。

解决方案 1

使用 BigDecimal 数据类型对货币值执行计算。使用 number format . getcurrency instance()helper 方法设置计算结果的格式。在下面的代码中,使用属于 BigDecimal 类的一些方法计算了三个货币值。然后,计算结果被转换为双精度值,并使用 NumberFormat 类进行格式化。首先,看看这些值是如何计算的:

BigDecimal currencyOne = new BigDecimal("25.65");
BigDecimal currencyTwo = new BigDecimal("187.32");
BigDecimal currencyThree = new BigDecimal("4.86");
BigDecimal result = null;
String printFormat = null;

// Add all three values
result = currencyOne.add(currencyTwo).add(currencyThree);
// Convert to double and send to formatDollars(), returning a String
printFormat = formatDollars(result.doubleValue());
System.out.println(printFormat);

// Subtract the first currency value from the second
result = currencyTwo.subtract(currencyOne);
printFormat = formatDollars(result.doubleValue());
System.out.println(printFormat);

接下来,让我们看看代码中使用的 formatDollars()方法。此方法接受双精度值,并使用基于美国地区的 NumberFormat 类对其执行格式化。然后,它返回一个表示货币的字符串值:

public static String formatDollars(double value){
    NumberFormat dollarFormat = NumberFormat.getCurrencyInstance(Locale.US);
    return dollarFormat.format(value);
}

正如您所看到的,NumberFormat 类允许根据指定的地区对货币进行格式化。如果您正在使用一个处理货币并具有国际范围的应用,这将非常方便。

$217.83
$161.67

解决方案 2

利用 Java Money API,这是 JSR 354 的重点,来执行货币计算。

注意

Java Money API 是在 https://jcp.org/en/jsr/detail?id=354 的 JSR 354 下开发的。它最初是为了在 Java 9 中完成和包含。然而,JSR 完成得相当早,并且不包含对 Java 9 代码库的依赖。因此,Java Money API 也可以用于旧版本的 Java,比如 Java 8,它可以在 http://javamoney.github.io/的 Github 上获得。

下面的示例演示如何使用执行货币计算和格式设置。Java Money API。

MonetaryAmount amount1 =  Money.of(25.65, Monetary.getCurrency("USD"));
MonetaryAmount amount2 =  Money.of(187.32, Monetary.getCurrency("USD"));
MonetaryAmount amount3 =  Money.of(4.86,Monetary.getCurrency("USD"));

MonetaryAmount result = null;
result = amount1.add(amount2).add(amount3);

MonetaryAmountFormat printFormat = MonetaryFormats.getAmountFormat(
    AmountFormatQuery.of(Locale.US));
System.out.println("Sum of all: " + printFormat.format(result));

result = amount2.subtract(amount1);
System.out.println("Subtract amount1 from amount 2: " + printFormat.format(result));

它是如何工作的

许多人在处理货币时试图使用不同的数字格式。虽然可以使用任何类型的数字对象来处理货币,但是在 Java 5 语言中添加了 BigDecimal 类,以帮助满足处理货币值的需求。我们将首先解释如何利用 BigDecimal 进行货币计算,因为这是一个经典的过程,然后我们将看一看 Java Money API。

BigDecimal 类最有用的特性可能是它提供了对舍入的控制。这就是为什么这样的类对于处理货币值如此有用。BigDecimal 类为舍入值提供了一个简单的 API,也使转换成 double 值变得容易,正如这个配方的解决方案所展示的那样。

注意

使用 BigDecimal 处理货币值是一种很好的做法。但是,这可能会牺牲一些性能。根据应用和性能要求,如果性能成为问题,可能值得使用 Math.round()来实现基本的舍入。

若要使用 BigDecimal 类提供特定的舍入,应使用 MathContext 对象或 RoundingMode 枚举值。在这两种情况下,都可以通过使用货币格式化解决方案(如解决方案示例中演示的解决方案)来省略这种精度。BigDecimal 对象内置了数学实现,因此执行这样的操作很容易。表 4-5 中描述了您可以使用的算术运算。

表 4-5。大十进制算术方法
|

方法

|

描述

|
| --- | --- |
| 添加() | 将一个 BigDecimal 对象值与另一个相加。 |
| 减去() | 从另一个 BigDecimal 对象值中减去一个 BigDecimal 对象值。 |
| 乘法() | 将一个 BigDecimal 对象的值乘以另一个。 |
| abs() | 返回给定 BigDecimal 对象值的绝对值。 |
| 功率 | 返回 BigDecimal 的 n 次幂;这个功率的计算精度是无限的。 |

执行完所需的计算后,对 BigInteger 对象调用 doubleValue()方法进行转换并获得双精度值。然后,您可以使用货币结果的 NumberFormat 类来格式化 double。

Java Money API 最初名为 JSR 354,旨在使 Java 语言中的货币操作更容易。该 API 为该语言带来了真正重大的变化,因为它最终允许人们以标准的方式对待货币,而不是以各种方式使用 BigDecimal。使用 Java Money API 的回报可能是巨大的,因为它可以使代码更容易阅读和理解,并提供货币结果,而不是必须强制转换为货币值的结果。

在解决方案 2 中,相同的货币值用于演示一些计算练习。API 中货币的标准类型是货币金额。在该解决方案中,您可以看到有三个 MonetaryAmount 对象,每个对象都使用 USD 货币表示不同的美元和美分值。为了获取存储在 MonetaryAmount 对象中的值,Money 实现类用于解析提供给它的值,然后返回指定货币类型的 MonetaryAmount 类型。Money 类使用 BigDecimal 存储数字值。

MonetaryAmount 接口提供了许多方法,可用于对存储值执行操作、与其他金额、精度等进行比较。具体来说,在解决方案中,您可以看到 add()方法接受另一个 MonetaryAmount,它用于将传入的值添加到原始 MonetaryAmount 中。另一个这样的方法是 subtract(),它从原始值中减去传递的值。

该解决方案还提供了有关格式化货币值的信息。MonetaryFormats 工厂可用于获取特定于所需地区的格式。然后,可以将生成的 MonetaryAmountFormat 模式应用于 MonetaryAmount,以相应地更改值的表示。

4-6.随机生成值

问题

您正在开发的应用需要使用随机生成的数字。

解决方案 1

使用 java.util.Random 类来帮助生成随机数。Random 类的开发目的是为一些 Java 数字数据类型生成随机数。这段代码演示了如何使用 Random 来生成这样的数字:

// Create a new instance of the Random class
Random random = new Random();

// Generates a random Integer
int myInt = random.nextInt();

// Generates a random Double value
double myDouble = random.nextDouble();

// Generates a random float
float myFloat = random.nextFloat();

// Generates a random Gaussian double
// mean 0.0 and standard deviation 1.0
// from this random number generator's sequence.
double gausDouble = random.nextGaussian();

// Generates a random Long
long myLong = random.nextLong();

// Generates a random boolean
boolean myBoolean = random.nextBoolean();

解决方案 2

利用 Math.random()方法。这将产生一个大于 0.0 但小于 1.0 的双精度值。下面的代码演示了此方法的用法:

double rand = Math.random();

它是如何工作的

java.util.Random 类使用 48 位种子来生成一系列伪随机值。正如您在这个配方的解决方案的例子中看到的,Random 类可以根据给定的种子生成许多不同类型的随机数值。默认情况下,种子是根据计算机处于活动状态的毫秒数的计算结果生成的。但是,可以使用 Random setSeed()方法手动设置种子。如果两个随机对象具有相同的种子,它们将产生相同的结果。

应该注意,在有些情况下,Random 类可能不是生成随机值的最佳选择。例如,如果您尝试使用 java.util.Random 的线程安全实例,那么在处理许多线程时,您可能会遇到性能问题。在这种情况下,您可以考虑使用 ThreadLocalRandom 类。要查看有关 ThreadLocalRandom 的更多信息,请参见位于docs . Oracle . com/javase/9/docs/API/Java/util/concurrent/threadlocalrrandom . html的文档。

类似地,如果您需要使用加密安全的随机对象,请考虑使用 secure Random。关于这个类的文档可以在docs . Oracle . com/javase/9/docs/API/Java/security/securerandom . html找到。

当您需要生成指定类型的随机值时,java.util.Random 类非常方便。它不仅易于使用,而且还为返回类型提供了广泛的选项。另一种简单的技术是使用 Math.random()方法,它产生一个介于 0.0 到 1.0 之间的 double 值,如解决方案 2 所示。这两种技术都提供了生成随机值的好方法。但是,如果需要生成特定类型的随机数,java.util.Random 是最佳选择。

4-7.获取不带时间的当前日期

问题

您正在开发一个应用,希望获得当前日期(不包括时间)并显示在表单上。

解决办法

利用日期-时间 API 获取当前日期。LocalDate 类表示年-月-日格式的 ISO 日历。下面几行代码捕获并显示当前日期:

LocalDate date = LocalDate.now();
System.out.println("Current Date:" + date);

它是如何工作的

日期-时间 API 使得获取当前日期变得容易,而不需要包括其他信息,比如时间。为此,导入 java.time.LocalTime 类并调用其 now()方法。LocalTime 类不能被实例化,因为它是不可变的和线程安全的。对 now()方法的调用返回另一个 LocalDate 对象,包含年-月-日格式的当前日期。

now()方法的另一个版本接受 java.time.Clock 对象作为参数,并根据该时钟返回日期。例如,下面几行代码演示了如何获取表示系统时间的时钟:

Clock clock = Clock.systemUTC();
LocalDate date = LocalDate.now(clock);

在以前的版本中,有其他方法可以获得当前日期,但通常时间是和日期一起出现的,然后必须进行格式化以删除不需要的时间数字。新的 java.time.LocalDate 类使得处理与时间无关的日期成为可能。

4-8.根据给定的日期条件获取日期对象

问题

您希望获得一个日期对象,给定一个年-月-日规格。

解决办法

为要获取对象的年、月和日调用 LocalDate.of()方法。例如,假设您想要获取 2000 年 11 月的指定日期的日期对象。您可以将该日期条件传递给 LocalDate.of()方法,如以下代码行所示:

LocalDate date = LocalDate.of(2000, Month.NOVEMBER, 11);
System.out.println("Date from specified date: " + date);

结果如下:

Date from specified date: 2000-11-11

它是如何工作的

LocalDate.of()方法接受三个值作为参数。这些参数代表年、月和日。year 参数始终被视为 int 值。month 参数可以表示为一个 int 值,它对应于一个表示月份的枚举。Month 枚举将返回每个月的 int 值,一月返回 1,十二月返回 12。因此,月。十一月返回 11。Month 对象也可以作为第二个参数传递,而不是作为 int 值。最后,通过将一个 int 值作为第三个参数传递给 of()方法来指定一个月中的第几天。

注意

有关月份枚举的更多信息,请参见位于download.java.net/jdk9/docs/api/java/time/Month.html的在线文档。

4-9.获取年-月-日日期组合

问题

您想要获取指定日期的年、年、月或月。

解决方案 1

要获取指定日期的年月,请使用 java.time.YearMonth 类。该类用于表示特定年份的月份。在以下代码行中,YearMonth 对象用于获取当前日期和另一个指定日期的年和月。

YearMonth yearMo = YearMonth.now();
System.out.println("Current Year and month:" + yearMo);      
YearMonth specifiedDate = YearMonth.of(2000, Month.NOVEMBER);
System.out.println("Specified Year-Month: " + specifiedDate);

结果如下:

Current Year and month:2014-12
Specified Year-Month: 2000-11

解决方案 2

要获得当前日期或指定日期的月-日,只需使用 java.time.MonthDay 类。下面几行代码演示了如何获取月-日组合。

MonthDay monthDay = MonthDay.now();
System.out.println("Current month and day: " + monthDay);        
MonthDay specifiedDate = MonthDay.of(Month.NOVEMBER, 11);
System.out.println("Specified Month-Day: " + specifiedDate);

结果如下:

Current month and day: --12-14
Specified Month-Day: --11-11

注意,默认情况下,MonthDay 不会返回非常有用的格式。有关格式化的更多帮助,请参见配方 4-17。

它是如何工作的

日期-时间 API 包括一些类,这些类使得获取应用需要的日期信息变得容易。其中两个是 YearMonth 和 MonthDay 类。YearMonth 类用于获取年月格式的日期。它包含了一些可以用来获得年月组合的方法。如解决方案中所示,您可以调用 now()方法来获取当前的年-月组合。与 LocalDate 类类似,YearMonth 也包含一个 of()方法,该方法接受 int 格式的年份和一个表示一年中月份的数字。在该解决方案中,Month 枚举用于获取月份值。

与 YearMonth 类类似,MonthDay 以月-日格式获取日期。它还包含一些不同的方法来获得月-日组合。解决方案 2 演示了两种这样的技术:通过调用 now()方法获得当前的月-日组合,并使用 of()方法获得指定日期的月-日组合。of()方法接受一个整数值作为第一个参数,第二个参数接受一个整数值作为第几天。

4-10.基于当前时间获取和计算时间

问题

您希望获得当前时间,以便可以用它来标记给定的记录。您还想基于该时间执行计算。

解决办法

使用 LocalTime 类来获取和显示当前时间,该类是新的日期时间 API 的一部分。在以下代码行中,演示了 LocalTime 类。

LocalTime time = LocalTime.now();
System.out.println("Current Time: " + time);

一旦获得了时间,就可以针对 LocalTime 实例调用方法来获得所需的结果。在以下代码行中,有一些使用 LocalTime 方法的示例:

// atDate(LocalDate): obtain the local date and time
LocalDateTime ldt = time.atDate(LocalDate.of(2011,Month.NOVEMBER,11));
System.out.println("Local Date Time object: " + ldt);

// of(int hours, int min): obtain a specific time
LocalTime pastTime = LocalTime.of(1, 10);

// compareTo(LocalTime): compare two times.  Positive
// return value returned if greater
System.out.println("Comparing times: " + time.compareTo(pastTime));

// getHour(): return hour in int value (24-hour format)
int hour = time.getHour();
System.out.println("Hour: " + hour);

// isAfter(LocalTime): return Boolean comparison
System.out.println("Is local time after pastTime? " + time.isAfter(pastTime));

// minusHours(int): Subtract Hours from LocalTime
LocalTime minusHrs = time.minusHours(5);
System.out.println("Time minus 5 hours: " + minusHrs);

// plusMinutes(int): Add minutes to LocalTime
LocalTime plusMins = time.plusMinutes(30);
System.out.println("Time plus 30 mins: " + plusMins);

结果如下:

Current Time: 22:21:08.419
Local Date Time object: 2011-11-11T22:21:08.419
Comparing times: 1
Hour: 22
Is local time after pastTime? true
Time minus 5 hours: 17:21:08.419
Time plus 30 mins: 22:51:08.419

它是如何工作的

有时需要获得当前系统时间。LocalTime 类可用于通过调用其 now()方法来获取当前时间。与 LocalDate 类类似,可以调用 LocalTime.now()方法来返回等于当前时间的 LocalTime 对象。LocalTime 类还包含几个可以用来操作时间的方法。解决方案中包含的示例提供了可用方法的简要概述。

让我们看一些例子,为如何调用 LocalTime 方法提供一些上下文。若要获取设置为特定时间的 LocalTime 对象,请调用 LocalTime.of(int,int)方法,传递表示小时和分钟的 int 参数。

// of(int hours, int min): obtain a specific time
LocalTime pastTime = LocalTime.of(1, 10);

atDate(LocalDate)实例方法用于将 LocalDate 对象应用于 LocalTime 实例,返回 LocalDateTime 对象(更多信息,请参见配方 4-11)。

LocalDateTime ldt = time.atDate(LocalDate.of(2011,Month.NOVEMBER,11));

有几种方法可以用来获得时间的部分。例如,getHour()、getMinute()、getNano()和 getSecond()方法可用于返回 LocalTime 对象的那些指定部分。

int hour = time.getHour();
int min  = time.getMinute();
int nano = time.getNano();
int sec  = time.getSecond();

也有几种比较方法可供使用。例如,compareTo(LocalTime)方法可用于将一个 LocalTime 对象与另一个进行比较。isAfter(LocalTime)可用于确定时间是否在另一个之后,isBefore(LocalTime)用于指定相反的时间。如果需要计算,有几种方法可用,包括:

  • 减号(长时间总量,时间单位)

  • 减去(临时金额)

  • 小时数(长)

  • 分钟(长)

  • 小写(长)

  • 毫秒(长型)

  • 加(long amountToAdd,temporalunit unit)

  • 加号(临时金额)

  • plusHours(长)

  • 多分钟(长)

  • 纳秒(长)

  • plusSeconds(长)

要查看 LocalTime 类中包含的所有方法,请参见位于docs . Oracle . com/javase/9/docs/API/Java/time/local time . html的在线文档。

4-11.获取并一起使用日期和时间

问题

在您的应用中,您不仅希望显示当前日期,还希望显示当前时间。

解决方案 1

利用 LocalDateTime 类来捕获和显示当前日期和时间,该类是新的日期时间 API 的一部分。LocalDateTime 类包含一个名为 now()的方法,该方法可用于同时获取当前日期和时间。下面几行代码演示了如何做到这一点:

LocalDateTime ldt = LocalDateTime.now();
System.out.println("Local Date and Time: " + ldt);

结果 LocalDateTime 对象包含日期和时间,但不包含时区信息。LocalDateTime 类还包含其他方法,这些方法提供了处理日期时间数据的选项。例如,若要返回具有指定日期和时间的 LocalDateTime 对象,请将 int 类型的参数传递给 LocalDateTime.of()方法,如下所示:

// Obtain the LocalDateTime object of the date 11/11/2000 at 12:00
LocalDateTime ldt2 = LocalDateTime.of(2000, Month.NOVEMBER, 11, 12, 00);

以下示例演示了 LocalDateTime 对象中可用的一些方法:

// Obtain the month from LocalDateTime object
Month month = ldt.getMonth();
int monthValue = ldt.getMonthValue();
System.out.println("Month: " + month);
System.out.println("Month Value: " + monthValue);

// Obtain day of Month, Week, and Year
int day = ldt.getDayOfMonth();
DayOfWeek dayWeek = ldt.getDayOfWeek();
int dayOfYr = ldt.getDayOfYear();
System.out.println("Day: " + day);
System.out.println("Day Of Week: " + dayWeek);
System.out.println("Day of Year: " + dayOfYr);

// Obtain year
int year = ldt.getYear();
System.out.println("Date: " + monthValue + "/" + day + "/" + year);

int hour = ldt.getHour();
int minute = ldt.getMinute();
int second = ldt.getSecond();
System.out.println("Current Time: " + hour + ":" + minute + ":" + second);

// Calculation of Months, etc.
LocalDateTime currMinusMonths = ldt.minusMonths(12);
LocalDateTime currMinusHours = ldt.minusHours(10);
LocalDateTime currPlusDays = ldt.plusDays(30);
System.out.println("Current Date and Time Minus 12 Months: " + currMinusMonths);
System.out.println("Current Date and Time MInus 10 Hours: " + currMinusHours);
System.out.println("Current Date and Time Plus 30 Days:" + currPlusDays);

结果如下:

Day: 28
Day Of Week: SATURDAY
Day of Year: 332
Date: 11/28/2015
Current Time: 10:23:8
Current Date and Time Minus 12 Months: 2014-11-28T10:23:08.399
Current Date and Time MInus 10 Hours: 2015-11-28T00:23:08.399
Current Date and Time Plus 30 Days:2015-12-28T10:23:08.399

解决方案 2

如果只需要获得当前日期而不需要进入日历细节,请使用 java.util.date 类来生成一个新的 Date 对象。这样做将生成一个等于当前系统日期的新日期对象。在下面的代码中,您可以看到创建一个新的 Date 对象并获取当前日期是多么容易:

Date date = new Date();

System.out.println("Using java.util.Date(): " + date);
System.out.println("Getting time from java.util.Date(): " + date.getTime());

结果将是一个 Date 对象,它包含从运行代码的系统中获取的当前日期和时间,包括时区信息,如下面的清单所示。时间是自 1970 年 1 月 1 日 00:00:00 GMT 以来的毫秒数。

Using java.util.Date(): Sat Nov 28 10:23:08 CST 2015
Getting time from java.util.Date(): 1448727788454

解决方案 3

如果需要更精确的日历,可以使用 java.util.Calendar 类。虽然使用 Calendar 类会使您的代码更长,但结果比使用 java.util.Date 更精确。以下代码演示了使用该类获取当前日期的一小部分功能:

Calendar gCal = Calendar.getInstance();

// Month is based upon a zero index, January is equal to 0,
// so we need to add one to the month for it to be in
// a standard format
int month = gCal.get(Calendar.MONTH) + 1;int day = gCal.get(Calendar.DATE);
int yr = gCal.get(Calendar.YEAR);

String dateStr = month + "/" + day + "/" + yr;
System.out.println(dateStr);

int dayOfWeek = gCal.get(Calendar.DAY_OF_WEEK);

// Print out the integer value for the day of the week
System.out.println(dayOfWeek);

int hour = gCal.get(Calendar.HOUR);
int min  = gCal.get(Calendar.MINUTE);
int sec = gCal.get(Calendar.SECOND);

// Print out the time
System.out.println(hour + ":" + min + ":" + sec);

// Create new DateFormatSymbols instance to obtain the String
// value for dates
DateFormatSymbols symbols = new DateFormatSymbols();
String[] days = symbols.getWeekdays();
System.out.println(days[dayOfWeek]);

// Get crazy with the date!
int dayOfYear = gCal.get(Calendar.DAY_OF_YEAR);
System.out.println(dayOfYear);

// Print the number of days left in the year
System.out.println("Days left in " + yr + ": " + (365-dayOfYear));

int week = gCal.get(Calendar.WEEK_OF_YEAR);
// Print the week of the year
System.out.println(week);

如这段代码所示,使用 Calendar 类时,可以获得关于当前日期的更详细的信息。运行代码的结果将如下所示:

11/28/2015
7
10:28:26
Saturday
332
Days left in 2015: 33
48
注意

尽管 java.util.Calendar 为获取精确的日期/时间信息提供了一种健壮的技术,但是从 java 8 开始,首选的解决方案是利用 Java 日期-时间 API。

它是如何工作的

许多应用需要使用当前日历日期。通常还需要获得当前时间。有不同的方法可以做到这一点,这个食谱的解决方案展示了其中的三种。Date-Time API 包括一个 LocalDateTime 类,它使您能够通过调用其 now()方法来捕获当前日期和时间。调用 LocalDateTime.of()时,可以通过指定相应的 int 和 Month 类型参数来获取指定的日期和时间。还有许多方法可以通过 LocalDateTime 实例使用,例如 getHours()、getMinutes()、getNanos()和 getSeconds(),这些方法允许对日期和时间进行更细粒度的控制。LocalDateTime 的实例还包含用于执行计算、转换、比较等操作的方法。为了简洁起见,这里没有列出所有的方法,但是提供了进一步的信息;请参考位于docs . Oracle . com/javase/9/docs/API/Java/time/local datetime . html的在线文档。这个菜谱的解决方案 1 演示了 LocalDateTime 的使用,展示了如何执行计算并获取日期和时间的一部分以供将来使用。

默认情况下,java.util.Date 类可以不带任何参数进行实例化,以返回当前日期和时间。Date 类也可以通过 getTime()方法返回一天中的当前时间。如解决方案中所述,getTime()方法返回自 1970 年 1 月 1 日 00:00:00 GMT 以来的毫秒数,由正在使用的 Date 对象表示。关于将当前日期和时间分解成更细粒度的时间间隔,还有其他几种方法可以针对 Date 对象调用。例如,Date 类有方法 getHours()、getMinutes()、getSeconds()、getMonth()、getDay()、getTimezoneOffset()和 getYear()。但是,不建议使用除 getTime()以外的任何方法,因为 java.time.LocalDateTime 和 java.util.Calendar get()方法都不赞成使用这些方法。当一个方法或类被弃用时,这意味着它不应该再被使用,因为它可能会在 Java 语言的未来版本中被删除。但是,Date 类中包含的一些方法还没有被标记为不推荐使用,所以 Date 类很可能会包含在 Java 的未来版本中。保持不变的方法包括 after()、before()、compareTo()、setTime()和 equals()等比较方法。这个菜谱的解决方案 2 演示了如何实例化一个 Date 对象并打印出当前的日期和时间。

如前所述,Date 类有许多方法已经过时,不应再使用。在这个方法的解决方案 3 中,java.util.Calendar 类被演示为获取大部分信息的后继类。Calendar 类是在 JDK 1.1 中引入的,当时许多 Date 方法都被弃用了。从解决方案 3 中可以看出,Calendar 类包含了 Date 类中包含的所有相同的功能,只是 Calendar 类更加灵活。Calendar 类实际上是一个包含方法的类,这些方法用于在特定时间和日期之间进行转换,并以各种方式操作日历。解决方案 3 中演示的 Calendar 就是这样一个类,它扩展了 Calendar 类,因此提供了这种功能。Calendar 类在 Java 8 中获得了一些新方法。表 4-6 中列出了 java.util.Calendar 中的新方法。

表 4-6。Java 8 中 java.util.Calendar 的新方法
|

方法名

|

描述

|
| --- | --- |
| getAvailableCalendarTypes() | 返回包含所有支持的日历类型的不可修改的集合。 |
| getCalendarType() | 返回此日历的日历类型。 |
| t 常量() | 转化为瞬间。 |

对于某些应用,Date 类可以很好地工作。例如,在处理时间戳时,Date 类会很有用。但是,如果应用需要日期和时间的详细操作,那么建议使用 LocalDateTime 或 Calendar 类,这两个类都包括 Date 类的所有功能以及更多特性。这个配方的所有解决方案在技术上都是合理的;选择最适合您的应用需求的一个。

4-12.获取机器时间戳

问题

您需要从系统获得一个基于机器的时间戳。

解决办法

利用一个 Instant 类,它表示基于机器时间的时间线上一纳秒的开始。在下面的示例中,使用了一个 Instant 来获取系统时间戳。在其他场景中也会用到该瞬间,例如在基于该瞬间计算不同日期时。

public static void instants(){
        Instant timestamp = Instant.now();
        System.out.println("The current timestamp: " + timestamp);

        //Now minus three days
        Instant minusThree = timestamp.minus(3, ChronoUnit.DAYS);
        System.out.println("Now minus three days:" + minusThree);

        ZonedDateTime atZone = timestamp.atZone(ZoneId.of("GMT"));
        System.out.println(atZone);

        Instant yesterday = Instant.now().minus(24, ChronoUnit.HOURS);
        System.out.println("Yesterday: " + yesterday);
    }

结果如下:

The current timestamp: 2015-11-28T16:21:42.197Z
Now minus three days:2015-11-25T16:21:42.197Z
2015-11-28T16:21:42.197Z[GMT]
Yesterday: 2015-11-27T16:21:42.273Z

它是如何工作的

Date-Time API 引入了一个名为 Instant 的新类,它表示基于机器的时间中时间轴上一纳秒的开始。基于机器时间,瞬间的值从纪元(1970 年 1 月 1 日 00:00:00Z)开始计数。纪元前的任何值都是负的,纪元后的值都是正的。Instant 类非常适合于获取机器时间戳,因为它包含所有相关的日期和时间信息,精确到纳秒。

Instant 类是静态且不可变的,因此要获得当前时间戳,可以调用 now()方法。这样做将返回当前瞬间的副本。Instant 还包括转换和计算方法,每个方法都返回 Instant 或其他类型的副本。在这个解决方案中,now()方法返回当前的时间戳,然后是几个示例,展示如何执行计算和获取即时信息。

Instant 是 Java 8 中一个重要的新特性,因为它使得处理当前时间和日期数据变得更加容易。其他日期和时间类,如 LocalDateTime,也很有用。然而,瞬间是最准确的时间戳,因为它是基于纳秒精度的。

4-13.基于时区转换日期和时间

问题

您正在开发的应用有可能在全世界得到应用。在应用的某些区域,需要显示静态日期和时间,而不是系统日期和时间。在这种情况下,需要对这些静态日期和时间进行转换,以适应应用用户当前所在的特定时区。

解决办法

日期-时间 API 通过时区和偏移类提供了处理时区数据的适当工具。在下面的场景中,假设应用正在处理租赁车辆的预订。你可以在一个时区租车,然后在另一个时区还车。下面几行代码演示了如何在这种情况下打印出个人的预订。以下名为 scheduleReport 的方法接受表示签入和签出日期/时间的 LocalDateTime 对象,以及每个对象的 ZoneIds。航空公司可以使用这种方法打印特定航班的时区信息。

public static void scheduleReport(LocalDateTime checkOut, ZoneId checkOutZone,
                           LocalDateTime checkIn, ZoneId checkInZone){

    ZonedDateTime beginTrip = ZonedDateTime.of(checkOut, checkOutZone);
    System.out.println("Trip Begins: " + beginTrip);

    // Get the rules of the check out time zone
    ZoneRules checkOutZoneRules = checkOutZone.getRules();
    System.out.println("Checkout Time Zone Rules: " + checkOutZoneRules);

    //If the trip took 4 days
    ZonedDateTime beginPlus = beginTrip.plusDays(4);
    System.out.println("Four Days Later: " + beginPlus);

    // End of trip in starting time zone
    ZonedDateTime endTripOriginalZone = ZonedDateTime.of(checkIn, checkOutZone);
    ZonedDateTime endTrip = ZonedDateTime.of(checkIn, checkInZone);
    int diff = endTripOriginalZone.compareTo(endTrip);
    String diffStr = (diff >= 0) ? "NO":"YES";
    System.out.println("End trip date/time in original zone: " + endTripOriginalZone);
    System.out.println("End trip date/time in check-in zone: " + endTrip );
    System.out.println("Original Zone Time is less than new zone time? " +
           diffStr );
    ZoneId checkOutZoneId = beginTrip.getZone();
    ZoneOffset checkOutOffset = beginTrip.getOffset();
    ZoneId checkInZoneId = endTrip.getZone();
    ZoneOffset checkInOffset = endTrip.getOffset();

    System.out.println("Check out zone and offset: " + checkOutZoneId + checkOutOffset);
    System.out.println("Check in zone and offset: " + checkInZoneId +  checkInOffset);

}

结果如下:

Trip Begins: 2015-12-13T13:00-05:00[US/Eastern]
Checkout Time Zone Rules: ZoneRules[currentStandardOffset=-05:00]
Four Days Later: 2015-12-17T13:00-05:00[US/Eastern]
End trip date/time in original zone: 2015-12-18T10:00-05:00[US/Eastern]
End trip date/time in check-in zone: 2015-12-18T10:00-07:00[US/Mountain]
Original Zone Time is less than new zone time? YES
Check out zone and offset: US/Eastern-05:00
Check in zone and offset: US/Mountain-07:00

它是如何工作的

时区给开发人员增加了另一个挑战,Java 日期时间 API 为使用时区提供了一个简单的方面。日期-时间 API 包括一个 java.time.zone 包,其中包含许多有助于处理时区数据的类。这些类为时区规则、数据以及本地时间线中的间隙和重叠提供支持,这些间隙和重叠通常是夏令时转换的结果。表 4-7 中列出了组成区域包的类别。

表 4-7。时区类别
|

类别名

|

描述

|
| --- | --- |
| ZoneId | 指定区域标识符并用于转换。 |
| ZoneOffset(区域偏移) | 指定格林威治/UTC 时间的时区偏移量。 |
| ZonedDateTime | 一个 date-time 对象,它还处理时区数据,该数据具有格林威治/UTC 时间的时区偏移量。 |
| 区域规则 | 定义特定时区的时区偏移量如何变化的规则。 |
| ZoneRulesProvider | 向特定系统提供时区规则。 |
| 区域偏移转换 | 由局部时间线中的不连续性引起的两个偏移之间的转换。 |
| ZoneOffsetTransitionRule | 表达如何创建过渡的规则。 |

从最基本的时区类 ZoneId 开始,每个时区都包含一个特定的时区标识符。此标识符对于将特定时区分配给日期时间非常有用。在该解决方案中,ZoneId 用于计算两个时区之间的任何差异。ZoneId 标识了基于特定偏移量的固定或基于地理区域的转换应使用的规则。有关 ZoneId 的更多详细信息,请参见位于docs . Oracle . com/javase/9/docs/API/Java/time/zoned datetime . html的文档。

ZonedDateTime 是一个不可变的类,用于同时处理日期-时间和时区数据。这个类表示一个包含 ZoneId 的对象,非常类似于 LocalDateTime。它可以用来表示日期的所有方面,包括年、月、日、小时、分钟、秒、毫微秒和时区。该类包含一组用于执行计算、转换等的方法。为了简洁起见,这里没有列出 ZonedDateTime 中包含的方法,但是您可以在位于docs . Oracle . com/javase/9/docs/API/Java/time/zoned datetime . html的文档中了解它们。

ZoneOffset 指定格林威治/UTC 时间的时区偏移量。您可以通过调用 ZonedDateTime.getOffset()方法来查找特定时区的偏移量。ZoneOffset 类包含一些方法,这些方法可以很容易地将偏移量分解为不同的时间单位。例如,getTotalSeconds()方法返回小时、分钟和秒字段的总和,作为可以添加到时间中的单个偏移量。有关更多信息,请参考位于docs . Oracle . com/javase/9/docs/API/Java/time/zone offset . html的在线文档。

可以定义许多规则来确定单个时区的时区偏移量如何变化。ZoneRules 类用于为区域定义这些规则。例如,可以调用 ZoneRules 来指定或确定夏令时是否是一个因素。还可以将 Instant 或 LocalDateTime 传递给 getOffset()和 getTransition()等 ZoneRules 方法,以返回 ZoneOffset 或 ZoneOffsetTransition。有关 ZoneRules 的更多信息,请参考位于docs . Oracle . com/javase/9/docs/API/Java/time/zone/zone rules . html的在线文档。

另一个经常使用的时区类是 ZoneOffsetTransition。这个类模拟了由于夏令时的变化而导致的春季和秋季偏移量之间的转换。它用于确定转场之间是否有间隙,获取转场的持续时间,等等。有关 ZoneOffsetTransition 的更多信息,请参见位于docs . Oracle . com/javase/9/docs/API/Java/time/zone/ZoneOffsetTransition . html的在线文档。

ZoneRulesProvider、ZoneOffsetTransitionRule 和其他类通常不像其他类那样经常用于处理日期和时区。这些类对于管理时区规则和转换的配置非常有用。

注意

java.time.zone 包中的类非常重要,因为每个类都可以调用许多方法。这个食谱提供了入门指南,只有时区使用的基本知识。有关更多详细信息,请参见联机文档。

4-14.比较两个日期

问题

您想要确定一个日期是否大于另一个日期。

解决办法

利用日期-时间 API 类中的 compareTo()方法之一。在下面的解决方案中,比较了两个 LocalDate 对象,并显示了相应的消息。

public static void compareDates(LocalDate ldt1,
            LocalDate ldt2) {
        int comparison = ldt1.compareTo(ldt2);
        if (comparison > 0) {
            System.out.println(ldt1 + " is larger than " + ldt2);
        } else if (comparison < 0) {
            System.out.println(ldt1 + " is smaller than " + ldt2);
        } else {
            System.out.println(ldt1 + " is equal to " + ldt2);
        }

    }

同样,在进行日期比较时,也有一些方便的方法。具体来说,isAfter()、isBefore()和 isEqual()方法可以像 compareTo()一样用于比较,如下面的清单所示。

public static void compareDates2(LocalDate ldt1, LocalDate ldt2){
    if(ldt1.isAfter(ldt2)){
        System.out.println(ldt1 + " is after " + ldt2);
    } else if (ldt1.isBefore(ldt2)){
        System.out.println(ldt1 + " is before " + ldt2);
    } else if (ldt1.isEqual(ldt2)){
        System.out.println(ldt1 + " is equal to " + ldt2);
    }
}

它是如何工作的

许多日期-时间 API 类都包含一个方法,用于比较两个不同的日期-时间对象。在本示例的解决方案中,LocalDate.compareTo()方法用于确定一个 LocalDate 对象是否大于另一个。如果第一个 LocalDate 大于第二个 LocalDate,则 compareTo()方法返回一个负 int 值,如果两者相等,则返回零,如果第二个 local date 大于第一个 local date,则返回一个正数。

每个包含 compareTo()的日期时间类都有相同的结果。也就是说,返回一个 int 值,指示第一个对象是大于、小于还是等于第二个对象。下面列出了包含 compareTo()方法的每个类:

  • 持续时间

  • 局部日期

  • LocalDateTime

  • LocalTime(本地时间)

  • 瞬间

  • 蒙特达伊

  • OffsetDateTime

  • 偏移时间

  • 年月

  • ZoneOffset(区域偏移)

如第二个清单所示,isAfter()、isBefore()和 isEqual()方法也可以用于比较。这些方法返回一个布尔值来指示比较结果。虽然这些方法的结果可以像 compareTo()一样用于执行日期比较,但是它们可以使代码更容易阅读。

4-15.寻找日期和时间之间的间隔

问题

您需要确定两个日期或时间之间过去了多少小时、几天、几周、几个月或几年。

解决方案 1

利用日期-时间 API 来确定两个日期之间的差异。具体来说,使用 Period 类来确定两个日期之间的时间段(以天为单位)。下面的示例演示如何获取两个日期之间的天数、月数和年数的间隔。

注意

此示例显示日、月和年的差异,但不显示两个日期之间的累计日或月。要确定两个日期之间的总累积天数、月数和年数,请继续阅读解决方案#2 和#3。

LocalDate anniversary = LocalDate.of(2000, Month.NOVEMBER, 11);
LocalDate today = LocalDate.now();
Period period = Period.between(anniversary, today);
System.out.println("Number of Days Difference: " +  period.getDays());
System.out.println("Number of Months Difference: " + period.getMonths());
System.out.println("Number of Years Difference: " + period.getYears());

结果如下:

Number of Days Difference: 16
Number of Months Difference: 1
Number of Years Difference: 13

解决方案 2

使用 java.util.concurrent.TimeUnit 枚举在给定日期之间执行计算。使用此枚举,可以获得天、小时、微秒、毫秒、分钟、纳秒和秒的整数值。这样做将允许您执行必要的计算。

// Obtain two instances of the Calendar class
Calendar cal1 = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();

// Set the date to 01/01/2010:12:00
cal2.set(2010,0,1,12,0);
Date date1 = cal2.getTime();
System.out.println(date1);

long mill = Math.abs(cal1.getTimeInMillis() - date1.getTime());
// Convert to hours
long hours = TimeUnit.MILLISECONDS.toHours(mill);
// Convert to days
Long days = TimeUnit.HOURS.toDays(hours);
String diff = String.format("%d hour(s) %d min(s)", hours,
TimeUnit.MILLISECONDS.toMinutes(mill) - TimeUnit.HOURS.toMinutes(hours));
System.out.println(diff);

diff = String.format("%d days", days);
System.out.println(diff);

// Divide the number of days by seven for the weeks
int weeks = days.intValue()/7;
diff = String.format("%d weeks", weeks);
System.out.println(diff);

这段代码的输出将被格式化,以显示指示当前日期和所创建的 date 对象之间的差异的文本字符串。

解决方案 3

若要确定以天、月、年或其他时间单位表示的总累积差异,请使用 ChronoUnit 类。下面的代码演示了如何利用 ChronoUnit 类来确定两个日期之间的天数和年数。

LocalDate anniversary = LocalDate.of(2000, Month.NOVEMBER, 11);
LocalDate today = LocalDate.now();
long yearsBetween = ChronoUnit.YEARS.between(anniversary, today);
System.out.println("Years between dates: " + yearsBetween);

long daysBetween = ChronoUnit.DAYS.between(anniversary, today);
System.out.println("Days between dates:" + daysBetween);

结果如下:

Years between dates: 13
Days between dates:4794

它是如何工作的

与大多数编程技术一样,用 Java 执行日期计算有多种方法。Java 8 中引入的日期-时间 API 包括一些用于确定时间间隔的新技术。Period 类用于确定指定对象的两个单位之间的差异周期。若要获取两个日期时间对象之间的时间段,请调用 Period.between()方法,传递两个要获取时间段的日期时间对象。周期有许多方法可以用来将间隔分解成不同的单位。例如,可以使用 getDays()方法获得两个日期-时间对象的周期天数。同样,可以调用 getMonths()和 getYears()方法来返回周期中的月数或年数。

日期-时间 API 还包括一个 ChronoUnit Enum,可用于 ISO 以外的日历系统,提供基于单位的访问来操作日期和时间。枚举中的每个单位值都包含许多用于执行操作的方法。其中一个方法是 between(),它只返回两个给定日期时间对象之间的指定时间单位。在解决方案中,它用于使用 ChronoUnit 返回年和日。YEARS.between()和计时单位。DAYS.between(),分别为。

最有用的技术之一是根据给定日期的时间(以毫秒为单位)执行计算。这提供了最精确的计算,因为它以非常小的时间间隔工作:毫秒。通过对 Calendar 对象调用 getTimeInMillis()方法,可以从该对象中获取以毫秒为单位的当前时间。同样,Date 对象将通过调用 getTime()方法返回以毫秒表示的值。正如您在这个食谱的解答中看到的,执行的第一个数学运算是给定日期之间的差值(以毫秒为单位)。获得该值,然后取其绝对值,将提供执行日期计算所需的基础。为了获得数字的绝对值,请使用 java.lang.Math 类中包含的 abs()方法,如以下代码行所示:

long mill = Math.abs(cal1\. getTimeInMillis() - date1.getTime());

绝对值将以长格式返回。可以使用 TimeUnit 枚举来获得不同的日期转换。它包含许多表示不同时间间隔的静态枚举常量值,类似于 Calendar 对象的那些值。这些值显示在这里。

注意

一个枚举类型是其字段由一组固定的常数值组成的类型。Java 语言在 1.5 版中欢迎枚举类型。

  • 小时

  • 微秒

  • 毫秒

  • 分钟

  • 纳秒

这些值本身就说明了它们所代表的转换间隔。通过对这些枚举调用转换方法,可以转换表示两个日期之间持续时间的长值。正如您在这个配方的解决方案中看到的,首先使用 enum 建立时间单位,然后对该时间单位进行转换调用。以下面的转换为例。第一,time unit 的时间单位。毫秒是成立的。其次,对其调用 toHours()方法,并将由 mill 字段表示的 long 值作为参数传递:

TimeUnit.MILLISECONDS.toHours(mill)

这段代码可以用英文翻译如下:“field mill 的内容用毫秒表示;将这些内容转换成小时。”该调用的结果将是将工厂字段中的值转换为小时。通过堆叠对 TimeUnit 的调用,可以进行更精确的转换。例如,以下代码将 mill 字段的内容转换为小时,然后转换为天:

TimeUnit.HOURS.toDays(TimeUnit.MILLISECONDS.toHours(mill))

同样,英文翻译可以读作,“场磨的内容以毫秒表示。将这些内容转换成小时。接下来,将这些小时转换成天数。”

TimeUnit 可以使时间间隔转换非常精确。将时间单位转换的精度与数学相结合,将允许您将两个日期的差异转换为任何时间间隔。

4-16.从指定的字符串中获取日期时间

问题

你想把一个字符串解析成一个日期时间对象。

解决办法

利用时态日期时间类的 parse()方法来解析使用预定义或自定义格式的字符串。下面几行代码演示了如何使用 parse()方法的变体将字符串解析为 date 或 date-time 对象。

// Parse a String to form a Date-Time object
LocalDate ld = LocalDate.parse("2014-12-28");
LocalDateTime ldt = LocalDateTime.parse("2014-12-28T08:44:00");
System.out.println("Parsed Date: " + ld);
System.out.println("Parsed Date-Time: " + ldt);

// Using a different Parser
LocalDate ld2 = LocalDate.parse("2014-12-28", DateTimeFormatter.ISO_DATE);
System.out.println("Different Parser: " + ld2);

// Custom Parser
String input = "12/28/2013";
try {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");
    LocalDate ld3 = LocalDate.parse(input, formatter);
    System.out.println("Custom Parsed Date: " + ld3);
} catch (DateTimeParseException ex){
    System.out.println("Not parsable: " + ex);
}

结果如下:

Parsed Date: 2014-12-28
Parsed Date-Time: 2014-12-28T08:44
Different Parser: 2014-12-28
Custom Parsed Date: 2014-12-28

它是如何工作的

Date-Time API 的时态类包括一个 parse()方法,该方法可用于使用指定的格式解析给定的输入字符串。默认情况下,parse()方法将根据目标对象的默认 DateTimeFormatter 进行格式化。例如,要解析字符串“2014-01-01”,可以调用默认的 LocalDate.parse()方法。

LocalDate date = LocalDate.parse("2014-01-01");

但是,可以将另一个 DateTimeFormatter 指定为 parse()方法的第二个参数。DateTimeFormatter 是用于格式化和打印日期和时间的最终类。它包含许多内置的格式化程序,可以指定这些格式化程序将字符串强制转换为日期-时间对象。例如,要基于不带偏移量的标准 ISO_DATE 格式进行解析,请调用 DateTimeFormatter。ISO_DATE,如该配方的解决方案中所示。有关 DateTimeFormatter 的更多信息,请参见位于docs . Oracle . com/javase/9/docs/API/Java/time/format/datetime formatter . html的在线文档。

通常,需要将文本字符串解析成日期-时间对象。许多核心日期-时间类都内置了 parse()方法,这使得这些任务变得很容易。

4-17.格式化日期以供显示

问题

您的应用需要使用特定的格式显示日期。您希望一次性定义该格式,并将其应用于所有需要显示的日期。

解决方案 1

利用 DateTimeFormatter 类(日期-时间 API 的一部分)根据您想要使用的模式来格式化日期和时间。DateTimeFormatter 类包含一个 ofPattern()方法,该方法接受一个字符串模式参数来指定所需的模式。每个时态日期时间类都包含一个 format()方法,该方法接受 DateTimeFormatter 并返回目标日期时间对象的基于字符串的格式。在以下代码行中,演示了 DateTimeFormatter:

DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("MMMM dd yyyy");

LocalDateTime now = LocalDateTime.now();
String output = now.format(dateFormatter);
System.out.println(output);

DateTimeFormatter dateFormatter2 = DateTimeFormatter.ofPattern("MM/dd/YY HH:mm:ss");
String output2 = now.format(dateFormatter2);
System.out.println(output2);

DateTimeFormatter dateFormatter3 = DateTimeFormatter.ofPattern("hh 'o''clock' a, zzzz");
ZonedDateTime zdt = ZonedDateTime.now();
String output3 = zdt.format(dateFormatter3);
System.out.println(output3);

结果如下:

December 28 2013
12/28/13 10:44:06
10 o'clock AM, Central Standard Time

解决方案 2

使用 java.util.Calendar 类获取所需的日期,然后使用 java.text.SimpleDateFormat 类格式化该日期。下面的示例演示 SimpleDateFormat 类的用法:

// Create new calendar
Calendar cal = Calendar.getInstance();

// Create instance of SimpleDateFormat class using pattern
SimpleDateFormat dateFormatter1 = new SimpleDateFormat("MMMMM dd yyyy");
String result = null;

result = dateFormatter1.format(cal.getTime());
System.out.println(result);

dateFormatter1.applyPattern("MM/dd/YY hh:mm:ss");
result = dateFormatter1.format(cal.getTime());
System.out.println(result);

dateFormatter1.applyPattern("hh 'o''clock' a, zzzz");
result = dateFormatter1.format(cal.getTime());
System.out.println(result);

运行此示例将产生以下结果:

June 22 2011
06/22/11 06:24:41
06 o'clock AM, Central Daylight Time

从结果中可以看出,DateTimeFormatter 和 SimpleDateFormat 类使得将日期转换成任何格式都很容易。

它是如何工作的

对于任何程序来说,日期格式都是一个常见的问题。人们喜欢在不同的情况下以特定的格式看到他们的约会。Java 语言包含几个方便的实用程序,用于正确格式化日期时间数据。具体来说,较新的 API 包括 DateTimeFormatter 类,Java SE 的早期版本包括 SimpleDateFormat 类,它们中的每一个都可以方便地执行格式化过程。

DateTimeFormatter 类是最后一个类,主要用于打印和格式化日期时间对象。若要获取可应用于对象的 DateTimeFormatter,请调用 DateTimeFormatter.ofPattern()方法,传递表示所需输出的基于字符串的模式。表 4-8 列出了可在基于字符串的模式中使用的不同模式字符。然后,通过调用对象的 format()方法并将 DateTimeFormatter 作为参数传递,可以将生成的 DateTimeFormatter 应用于任何时态日期时间对象。结果将是根据指定模板模式格式化的日期时间对象。

表 4-8。模式字符
|

性格;角色;字母

|

描述

|
| --- | --- |
| G | 时代 |
| y | 年 |
| Y | 周年 |
| M | 一年中的月份 |
| w | 一年中的周 |
| W | 月中的周 |
| D | 一年中的每一天 |
| d | 一个月中的第几天 |
| F | 一个月中的星期几 |
| E | 星期几的名称 |
| u | 一周中的天数 |
| a | 上午/下午 |
| H | 一天中的小时(0–23) |
| k | 一天中的小时数(1–24) |
| K | 上午/下午的小时数(0–11) |
| h | 上午/下午的小时数(1–12) |
| m | 小时中的分钟 |
| s | 分钟秒 |
| S | 毫秒 |
| z | 通用时区 |
| Z | RFC 822 时区 |
| X | ISO 8601 时区 |

SimpleDateFormat 类是在 Java 的早期版本中创建的,因此您不必为给定的日期执行手动翻译。

注意

不同的地区使用不同的日期格式,SimpleDateFormat 类简化了特定于地区的格式。

若要使用类,必须通过将基于字符串的模式作为参数传递给构造函数或者不向构造函数传递任何参数来实例化实例。基于字符串的模式提供了一个应该应用于给定日期的模板,然后返回一个以给定模式样式表示日期的字符串。一个模式由许多不同的字符串在一起组成。表 4-8 显示了可以在一个模式中使用的不同字符。

任何模式字符都可以放在一个字符串中,然后传递给 SimpleDateFormat 类。如果该类在没有传递模式的情况下被实例化,则可以稍后使用该类的 applyPattern()方法来应用该模式。当您想要更改一个实例化的 SimpleDateFormat 对象的模式时,applyPattern()方法也很方便,如这个配方的解决方案所示。以下代码摘录演示了模式的应用:

SimpleDateFormat dateFormatter1 = new SimpleDateFormat("MMMMM dd yyyy");
dateFormatter1.applyPattern("MM/dd/YY hh:mm:ss");

一旦将模式应用于 SimpleDateFormat 对象,就可以将表示时间的长值传递给 SimpleDateFormat 对象的 Format()方法。format()方法将返回使用所应用的模式格式化的给定日期\时间。然后,可以根据应用的需要使用基于字符串的结果。

4-18.编写可读的数字文本

问题

您的应用中的一些数值很长,您想让它看起来更容易判断一个数字有多大。

解决办法

在较大的数字中用下划线代替逗号或小数,以使它们更具可读性。下面的代码显示了一些通过使用下划线代替逗号来提高数值可读性的示例:

int million = 1_000_000;
int billion = 1_000_000_000;
float ten_pct = 1_0f;
double exp = 1_234_56.78_9e2;
注意

小数点值将自动默认为双精度值,除非使用尾随的“f”来表示该值是浮点数。

它是如何工作的

有时处理大量数据会变得很麻烦,很难读懂。自从 Java 7 发布以来,为了使代码更容易阅读,下划线现在可以和数字一起使用。下划线可以出现在数字文本中数字之间的任何位置。这允许使用下划线代替逗号或空格来分隔数字,使它们更容易阅读。

注意

下划线不能放在数字的开头或结尾,小数点或浮点文字附近,后缀 FL 之前,或者应该是数字串的位置。

4-19.声明二进制文本

问题

您正在开发一个需要声明二进制数的应用。

解决办法

利用二进制文字使你的代码可读。下面的代码段演示了二进制文本的用法:

int bin1 = 0b1100;
short bin2 = 0B010101;
short bin3 = (short) 0b1001100110011001;
System.out.println(bin1);
System.out.println(bin2);
System.out.println(bin3);

这将导致以下输出:

12
21
-26215

它是如何工作的

随着 Java 7 的发布,二进制文字成为了 Java 语言的一部分。byte、short、int 和 long 类型可以用二进制数字系统来表示。这个特性有助于在代码中更容易识别二进制数。为了使用二进制格式,只需在数字前面加上 0b 或 0B。

摘要

数字和日期在大多数应用中起着不可或缺的作用。Java 语言提供了大量的类,可以用来处理不同种类的数字,并对它们进行格式化以适应大多数情况。本章回顾了一些可用于舍入和格式化数字以及生成随机值的技术。Java 8 的发布引入了一个日期和时间包,为获取和处理日期带来了一个令人耳目一新、易于使用的 API。这一章讲述了新的日期和时间包的基础知识,更多内容可以在网上找到:docs.oracle.com/javase/tutorial/datetime/

五、面向对象的 Java

自从应用开发的第一天以来,编程语言已经发生了很大的变化。过去,过程语言是最先进的;事实上,今天仍有成千上万的 COBOL 和其他程序应用在使用。随着时间的推移,编码变得更加高效,重用、封装、抽象和其他面向对象的特性成为应用开发的关键。随着语言的发展,它们开始融入在程序中使用对象的思想。早在 20 世纪 70 年代,Lisp 语言就引入了一些面向对象的技术,但是真正的面向对象编程直到 20 世纪 90 年代才大获成功。

面向对象的程序由许多不同的代码组成,它们一起协同工作。面向对象的哲学不是编写包含一长串语句和命令的程序,而是将功能分解成独立的有组织的对象。每个对象都包含与其相关的功能,当这些对象被组合在一起时,它们可以被用来开发复杂的解决方案。随着人们注意到面向对象等同于生产力,诸如使用方法封装功能和重用另一个类的功能等编程技术开始流行起来。

在这一章中,我们将触及 Java 语言的一些关键的面向对象的特性。从涵盖访问修饰符的基本方法,到处理内部类的高级方法,本章包含的方法将帮助你理解 Java 的面向对象方法。

5-1.控制对类成员的访问

问题

你想创建一个不能从任何其他类访问的类的成员。

解决办法

创建私有实例成员,而不是将它们提供给其他类(公共的或受保护的)。例如,假设您正在创建一个应用,它将用于管理一项运动的一组运动员。您创建了一个名为 Player 的类,它将用于表示团队中的一名球员。您不希望从任何其他类访问该类的字段。下面的代码演示了一些实例成员的声明,使它们只能从定义它们的类中访问。

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

它是如何工作的

若要将类成员指定为私有,请使用 private 关键字作为其声明或签名的前缀。private 访问修饰符用于隐藏类的成员,这样外部类就不能访问它们。任何被标记为私有的类成员将只对同一类的其他成员可用。任何外部类都不能访问被指定为私有的字段或方法,使用代码完成的集成开发环境(ide)也不能看到它们。

正如在这个方法的解决方案中提到的,在声明一个类的成员时,有三种不同的访问修饰符可以使用。这些修饰符是公共的、受保护的和私有的。声明为公共的成员可用于任何其他类。那些被声明为受保护的类可用于同一个包中的任何其他类。最好只将那些需要从另一个类直接访问的类成员声明为 public 或 protected。使用 private access 修饰符隐藏类的成员有助于实施更好的面向对象。

5-2.使私有字段可供其他类访问

问题

您希望创建私有实例成员,以便外部类不能直接访问它们。但是,您也希望以受控的方式访问这些私有成员。

解决办法

通过设置 getters 和 setters 来访问私有字段,从而封装私有字段。下面的代码演示了私有字段的声明,后面是可用于从外部类获取或设置该字段值的访问器(getter)和赋值器(setter)方法:

private String firstName = null;
/**
 * @return the firstName
 */
public String getFirstName() {
 return firstName;
}

/**
 * @param firstName the firstName to set
 */
public void setFirstName(String firstName) {
    this.firstName = firstName;
}

外部类可以使用 getFirstName()方法来获取 FirstName 字段的值。同样,外部类可以使用 setFirstName(String firstName)方法来设置 FirstName 字段的值。

它是如何工作的

通常,当字段在类中被标记为私有时,它们仍然需要被外部类访问,以便设置或检索它们的值。为什么不直接处理这些字段,然后将它们公开呢?直接处理其他类的字段并不是好的编程实践,因为通过使用访问器(getters)和赋值器(setters),可以以受控的方式授予访问权限。通过不直接针对另一个类的成员进行编码,您还可以帮助分离代码,这有助于确保如果一个对象发生更改,依赖于它的其他对象不会受到负面影响。正如您在这个菜谱的解决方案的例子中所看到的,隐藏字段并使用公共方法来访问这些字段是相当容易的。简单地创建两个方法;一个用于获取私有字段的值,即“getter”或访问器方法。另一个用于设置私有字段的值,即“setter”或 mutator 方法。在这个配方的解决方案中,getter 用于返回私有字段中包含的未更改的值。类似地,setter 用于设置私有字段的值,方法是接受与私有字段具有相同数据类型的参数,然后将私有字段的值设置为该参数的值。

使用 getters 或 setters 访问字段的类不知道方法背后的任何细节。例如,如果需要,getter 或 setter 方法可以包含更多的功能。此外,可以更改这些方法的细节,而无需更改访问它们的任何代码。

注意

使用 getters 和 setters 并不能完全分离代码。事实上,许多人认为使用 getters 和 setters 不是一个好的编程实践。使用访问器方法的对象仍然需要知道它们正在处理的实例字段的类型。也就是说,getters 和 setters 是提供对对象私有实例字段的外部访问的标准技术。要以更面向对象的方式使用访问器方法,请在接口中声明它们,并针对接口而不是对象本身进行编码。有关接口的更多信息,请参考配方 5-6。

5-3.创建具有单个实例的类

问题

您希望创建一个在整个应用中只能有一个实例的类,这样所有应用用户都可以与该类的同一个实例进行交互。

解决方案 1

使用单例模式创建类。实现 Singleton 模式的类只允许该类的一个实例,并提供对该实例的单点访问。假设您想要创建一个统计类,用于计算一项有组织的运动中每个队和运动员的统计数据。在应用中拥有这个类的多个实例是没有意义的,所以您希望将 Statistics 类创建为一个 Singleton,以防止生成多个实例。下列类别代表单一模式:

package org.java9recipes.chapter5.recipe5_03;

import java.util.ArrayList;
import java.util.List;
import java.io.Serializable;

public class Statistics implements Serializable {

// Definition for the class instance
private static volatile Statistics instance = new Statistics();

private List teams = new ArrayList();

/**
 * Constructor has been made private so that outside classes do not have
 * access to instantiate more instances of Statistics.
 */
private Statistics(){
}

/**
 * Accessor for the statistics class.  Only allows for one instance of the
 * class to be created.
 * @return
 */
public static Statistics getInstance(){

    return instance;
}

/**
 * @return the teams
 */
public List getTeams() {
    return teams;
}

/**
 * @param teams the teams to set
 */
public void setTeams(List teams) {
    this.teams = teams;
}
protected Object readResolve(){
        return instance;
    }
} 

如果另一个类试图创建该类的一个实例,它将使用 getInstance()访问器方法来获取 Singleton 实例。值得注意的是,解决方案代码演示了急切实例化,这意味着实例将在加载单例时被实例化。对于惰性实例化,它将在第一次请求时被实例化,您必须注意同步 getInstance()方法以使它是线程安全的。下面的代码演示了一个惰性实例化的示例:

public static Statistics getInstance(){
    synchronized(Statistics.class){
        if (instance == null){
            instance = new Statistics();
        }
    }
    return instance;
}

解决方案 2

首先,创建一个枚举,并在其中声明一个名为 INSTANCE 的元素。接下来,在枚举中声明其他字段,这些字段可用于存储应用所需的值。以下枚举表示将提供与解决方案 1 相同功能的单例:

import java.util.ArrayList;
import java.util.List;

public enum StatisticsSingleton {
    INSTANCE;

    private List teams = new ArrayList();

    /**
     * @return the teams
     */
    public List getTeams() {
        return teams;
    }

    /**
     * @param teams the teams to set
     */
    public void setTeams(List teams) {
        this.teams = teams;
    }
}
注意

recipe5_03 包中有一个测试类,您可以使用它来处理 enum Singleton 解决方案。

它是如何工作的

Singleton 模式用于创建不能被任何其他类实例化的类。当您只想将某个类的一个实例用于整个应用时,这很有用。可以通过以下三个步骤将单例模式应用于一个类。首先,将类的构造函数设为私有,这样外部类就不能实例化它。接下来,定义一个私有静态 volatile 字段,它将表示该类的一个实例。volatile 关键字保证每个线程使用相同的实例。创建类的实例,并将其分配给字段。在该配方的解决方案中,类名为 Statistics,字段定义如下:

private static volatile Statistics instance = new Statistics();

最后,实现一个名为 getInstance()的访问器方法,该方法只返回实例字段。下面的代码演示了这样一种访问器方法:

public static Statistics getInstance(){
    return instance;
}

要使用另一个类中的 Singleton,调用 Singleton 的 getInstance()方法。这将返回类的一个实例。下面的代码显示了另一个类的示例,该类获取了该配方的解决方案 1 中定义的 Statistics Singleton 的一个实例。

Statistics statistics = Statistics.getInstance();
List teams = statistics.getTeams();

任何调用该类的 getInstance()方法的类都将获得相同的实例。因此,对于整个应用中对 getInstance()的每次调用,Singleton 中包含的字段都具有相同的值。

如果单例被序列化然后反序列化会发生什么?这种情况可能会导致在反序列化时返回对象的另一个实例。为了防止此问题发生,请确保实现 readResolve()方法,如解决方案 1 中所示。当对象被反序列化时调用此方法,简单地返回实例可以确保不会生成另一个实例。

解决方案 2 展示了一种创建单例的不同方法,即使用 Java enum 而不是类。使用这种方法是有益的,因为 enum 提供序列化,禁止多重实例化,并允许您更简洁地处理代码。为了实现枚举单例,创建一个枚举并声明一个实例元素。这是一个静态常数,它将把枚举的实例返回给引用它的类。然后,您可以将元素添加到应用中的其他类可以用来存储值的枚举中。

与任何编程解决方案一样,有不止一种方法来做事情。有些人认为解决方案 1 中展示的标准单例模式不是最理想的解决方案。其他人出于不同的原因不喜欢 enum 解决方案。这两种方法都可以,尽管你可能会发现在某些情况下一种比另一种更有效。

5-4.生成类的实例

问题

在您的一个应用中,您想提供动态生成对象实例的能力。对象的每个实例都应该可以使用,对象创建者不需要知道对象创建的细节。

解决办法

利用工厂方法模式实例化类的实例,同时从对象创建者那里抽象出创建过程。创建工厂将使类的新实例能够在调用时返回。下面的类表示一个简单的工厂,它在每次调用其 createPlayer(String)方法时返回 Player 子类的一个新实例。返回的 Player 子类取决于传递给 createPlayer 方法的字符串值。

public class PlayerFactory {

    public static Player createPlayer(String playerType){
        Player returnType;
        switch(playerType){
        case "GOALIE":
            returnType = new Goalie();
            break;
        case "LEFT":
            returnType = new LeftWing();
            break;
        case "RIGHT":
            returnType = new RightWing();
            break;
        case "CENTER":
            returnType = new Center();
            break;
        case "DEFENSE":
            returnType = new Defense();
            break;
        default:
            returnType = new AllPlayer();
        }
        return returnType;
    }
}

如果一个类想要使用该工厂,它只需调用静态 createPlayer 方法,传递一个表示 Player 新实例的字符串值。下面的代码代表了一个 Player 子类;其他的可能非常相似:

public class Goalie extends Player implements PlayerType {

    private int totalSaves;

    public Goalie(){
        this.setPosition("GOALIE");
    }

    /**
     * @return the totalSaves
     */
    public int getTotalSaves() {
        return totalSaves;
    }

    /**
     * @param totalSaves the totalSaves to set
     */
    public void setTotalSaves(int totalSaves) {
        this.totalSaves = totalSaves;
    }
}

其他每个球员子类都非常类似于守门员类。需要注意的最重要的代码是工厂方法 createPlayer,它可用于创建 Player 类的新实例。

注意

为了进一步说明这个例子,您可以限制可以访问的方法。通过返回 PlayerType 类型的对象,并且只在该接口中声明可访问的方法,可以做到这一点。

它是如何工作的

工厂用于生成对象。它们通常用于从对象的创建者那里抽象出对象的实际创建。当创建者不需要知道生成新对象的实际实现细节时,这非常方便。当需要对对象的创建进行受控访问时,工厂模式也很有用。为了实现工厂,创建一个包含至少一个用于返回新创建的对象的方法的类。

在这个配方的解决方案中,PlayerFactory 类包含一个名为 createPlayer(String)的方法,该方法返回一个新创建的 Player 对象。这个方法在幕后不做任何特别的事情;它只是根据传递给该方法的字符串值实例化一个新的播放器实例。另一个可以访问 PlayerFactory 类的对象可以使用 createPlayer 返回新的 Player 对象,而无需知道该对象是如何创建的。虽然在 createPlayer 方法的情况下这并没有隐藏太多,但是 PlayerFactory 抽象了正在实例化的类的细节,因此开发人员只需担心如何获得新的 Player 对象。

工厂模式是控制如何创建对象的一种有效方式,它使创建某种类型的对象变得更加容易。想象一下,如果一个对象的构造函数接受的不仅仅是几个参数;创建不仅仅需要几个参数的新对象会变得很麻烦。生成一个工厂来创建这些对象,这样您就不必对每个实例化的所有参数进行硬编码,这样可以提高您的工作效率!

5-5.创建可重用对象

问题

您希望生成一个对象,用于表示应用中的某些内容。此外,您希望能够重用该对象来表示多个实例。例如,假设您正在创建一个应用,用于为不同的运动队生成统计数据和联盟信息。在这种情况下,您想要创建一个可以用来表示团队的对象。

解决办法

创建一个 JavaBean,它可以用来表示您想要创建的对象。JavaBean 对象提供了将对象字段声明为私有的能力,并且它们还允许读取和更新属性,以便可以在应用中传递和使用对象。这个菜谱演示了一个名为 Team 的 JavaBean 的创建。团队对象包含几个不同的字段,这些字段可以包含信息:

public class Team implements TeamType {

    private List<Player> players;
    private String name = null;
    private String city = null;

    /**
     * @return the players
     */
    public List<Player> getPlayers() {
        return players;
    }

    /**
     * @param players the players to set
     */
    public void setPlayers(List<Player> players) {
        this.players = players;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the city
     */
    public String getCity() {
        return city;
    }

    /**
     * @param city the city to set
     */
    public void setCity(String city) {
        this.city = city;
    }

}

正如您所看到的,这个解决方案中的对象包含三个字段,每个字段都被声明为 private。然而,每个字段都有两个访问器方法――getter 和 setter――允许字段被间接访问。

它是如何工作的

JavaBean 是一个用来保存信息的对象,这样就可以在应用中传递和使用信息。JavaBean 最重要的方面之一是它的字段被声明为私有的。这禁止其他类直接访问这些字段。相反,每个字段应该由定义的方法封装,以便其他类可以访问它们。这些方法必须遵循以下命名约定:

  • 用于访问字段数据的方法应该使用前缀 get,后跟字段名来命名。

  • 用于设置字段数据的方法应该使用前缀 set 命名,后跟字段名。

例如,在这个菜谱的解决方案中,Team 对象包含一个包含玩家姓名的字段。为了访问该字段,应该声明一个名为 getPlayers 的方法。该方法应该返回包含在玩家字段中的数据。同样,要填充 players 字段,应该声明一个名为 setPlayers 的方法。该方法应该接受与 players 字段类型相同的参数,并且应该将 players 字段的值设置为等于该参数。这可以在下面的代码中看到:

public List<Player> getPlayers() {
    return players;
}

void setPlayers(List<Player> players) {
    this.players = players;
}

JavaBeans 可用于填充数据列表、写入数据库记录或用于无数其他功能。使用 JavaBeans 使得代码更容易阅读和维护。它还有助于增加未来代码增强的可能性,因为只需要很少的代码实现。使用 JavaBeans 的另一个好处是大多数主流 ide 会自动完成字段的封装。

5-6.为类定义接口

问题

您希望创建一组方法签名和字段,这些方法签名和字段可以用作公共模板来公开类实现的方法和字段。

解决办法

生成一个 Java 接口来声明一个类必须实现的每个字段和方法。这样的接口可以由一个类实现,并用来表示一个对象类型。以下代码是一个接口,用于声明 Team 对象必须实现的方法:

public interface TeamType {

    void setPlayers(List<Player> players);
    void setName(String name);
    void setCity(String city);
    String getFullName();
}

接口中的所有方法都是隐式抽象的。也就是说,只提供了方法签名。还可以在接口中包含静态最终字段声明。

它是如何工作的

Java 接口是一种用于定义结构的构造,无论是类必须实现的字段还是方法。在大多数情况下,接口不包括任何方法实现;相反,它们只包含方法签名。接口可以包含隐式静态和最终变量。

注意

从 Java SE 8 开始,接口可以包含方法实现。这种方法被称为默认方法。更多详情见制作方法 5-7。

在这个配方的解决方案中,接口不包括任何常量字段声明。但是,它包括四个方法签名。所有方法签名都没有指定访问修饰符,因为接口中的所有声明都是隐式公共的。接口用于公开一组功能;因此,接口中公开的所有方法都必须是隐式公共的。任何实现接口的类都必须为接口中声明的任何方法签名提供实现,除了默认方法和抽象类(更多细节见方法 5-7 和 5-13),在这种情况下,接口可以为它的一个子类留下实现。

虽然 Java 语言不允许多重继承,但是一个 Java 类可以实现多个接口,从而允许受控形式的多重继承。抽象类也可以实现接口。下面的代码演示了一个实现接口的类:Team 对象声明实现了 TeamType 接口。

public class Team implements TeamType {

    private List<Player> players;
    private String name;
    private String city;

    /**
     * @return the players
     */
    public List<Player> getPlayers() {
        return players;
    }

    /**
     * @param players the players to set
     */
    public void setPlayers(List<Player> players) {
        this.players = players;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
} 

    /**
     * @return the city
     */
    public String getCity() {
        return city;
    }

    /**
     * @param city the city to set
     */
    public void setCity(String city) {
        this.city = city;
    }

public String getFullName() {
        return this.name + " - " + this.city;
    }

}

接口可以用来声明对象的类型。任何声明为具有接口类型的对象都必须遵循接口中声明的所有实现,除非存在默认实现。例如,下面的字段声明定义了一个包含所有在 TeamType 接口中声明的属性的对象:

TeamType team;

接口也可以扩展其他接口(因此多重继承提供了相同类型的理论)。但是,由于接口中没有方法实现,因此在 Java 类中实现多个接口比在 C++中扩展多个类要安全得多。

接口是 Java 语言最重要的结构之一。它们提供了用户和类实现之间的接口。尽管不使用接口也可以创建完整的应用,但它们有助于促进面向对象,并对其他类隐藏方法实现。

5-7.在不破坏现有代码的情况下修改接口

问题

您已经有了一个实现接口的实用程序类,并且实用程序库中的许多不同的类都实现了该接口。假设您想向实用程序类添加一个新方法,并通过它的接口使它可供其他类使用。但是,如果更改接口,可能会破坏一些已经实现该接口的现有类。

解决办法

将新方法及其实现作为默认方法添加到实用程序类接口中。通过这样做,实现该接口的每个类将自动获得对新方法的使用,并且不会被强制实现它,因为存在默认实现。下面的类接口包含一个默认方法,任何实现该接口的类都可以使用该方法。

public interface TeamType {

    List<Player> getPlayers();

    void setPlayers(List<Player> players);

    void setName(String name);

    void setCity(String city);

    String getFullName();

    default void listPlayers() {
        getPlayers().stream().forEach((player) -> {
            System.out.println(player.getFirstName() + " " + player.getLastName());
        });
    }

}

接口 TeamType 包含一个名为 listPlayers()的默认方法。这个方法不需要由任何实现 TeamType 的类来实现,因为接口中包含了一个默认的实现。

它是如何工作的

在以前的 Java 版本中,接口只能包含方法签名和常量变量。不可能在接口中定义方法实现。这在大多数情况下工作良好,因为接口是一种旨在加强类型安全和抽象实现细节的构造。但是,在某些情况下,允许接口包含默认方法实现是有益的。例如,如果有许多类实现了一个现有的接口,那么如果该接口被更改,许多代码可能会被破坏。这将造成向后兼容不可能的情况。在这种情况下,将一个默认的方法实现放在一个接口中是有意义的,而不是强制所有的类实现一个放在接口中的新方法。这就是为什么缺省方法变得必不可少,并且包含在 Java 8 版本中的原因。

要在接口中创建默认方法(也称为“defender 方法”),请在方法签名中使用关键字 default,并包含一个方法实现。一个接口可以包含零个或多个默认方法。在这个配方的解决方案中,listPlayers()方法是 TeamType 接口中的默认方法,任何实现 TeamType 的类都将自动继承默认实现。理论上,任何实现 TeamType 的类都不会受到 listPlayers()默认方法的影响。这使人们能够在不破坏向后兼容性的情况下更改接口,这具有很大的价值。

注意

从 Java 9 开始,可以在接口中创建私有方法。私有方法只能由同一接口中的默认方法使用。因此,如果您有一些在两个或更多默认方法中重复的代码,那么可重复的代码可以封装在私有方法中。

5-8.用不同的值构造同一类的实例

问题

您的应用需要能够构造同一对象的实例,但是每个对象实例需要包含不同的值,从而创建同一对象的不同类型。

解决办法

利用构建器模式,通过一步一步的过程构建同一对象的不同类型。例如,假设您有兴趣为一个体育联盟创建不同的团队。每个团队必须包含相同的属性,但是这些属性的值因团队而异。因此,您创建了许多相同类型的对象,但是每个对象都是唯一的。下面的代码演示了 builder 模式,该模式可用于创建所需的团队。

首先,您需要定义每个团队需要包含的一组属性。为此,应该创建一个 Java 接口,包含需要应用于每个团队对象的不同属性。以下是这种界面的一个示例:

public interface TeamType {

    public void setPlayers(List<Player> players);
    public void setName(String name);
    public void setCity(String city);
    public String getFullName();

}

接下来,定义一个类来代表一个团队。这个类需要实现刚刚创建的 TeamType 接口,这样它将遵循构建团队所需的格式:

public class Team implements TeamType {

    private List<Player> players;
    private String name = null;
    private String city = null;
    private int wins = 0;
    private int losses = 0;
    private int ties = 0;

    /**
     * @return the players
     */
    public List<Player> getPlayers() {
        return players;
    }

    /**
     * @param players the players to set
     */
    public void setPlayers(List<Player> players) {
        this.players = players;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the city
     */
    public String getCity() {
        return city;
    }

    /**
     * @param city the city to set
     */
    public void setCity(String city) {
        this.city = city;
    }

    public String getFullName(){
        return this.name + "  – "  + this.city;  
    }

} 

现在已经定义了团队类,需要创建一个构建器。构建器对象的目的是允许逐步创建团队对象。为了抽象构建对象的细节,应该创建一个构建器类接口。该接口应该定义将用于构建对象的任何方法,以及将返回完全构建的对象的方法。在这种情况下,该接口将定义构建新团队对象所需的每个方法,然后构建器实现将实现该接口。

public interface TeamBuilder {
    public void buildPlayerList();
    public void buildNewTeam(String teamName);
    public void designateTeamCity(String city);
    public Team getTeam();

} 

下面的代码演示了生成器类的实现。虽然下面的代码不会创建一个定制的播放器列表,但是它包含了实现构建器模式所需的所有特性。创建一个更加定制的球员名单的细节可以在以后解决,可能是通过允许用户通过键盘输入来创建球员。此外,TeamBuilder 接口可以用于实现不同运动的团队。下面的类命名为 HockeyTeamBuilder,但是实现 TeamBuilder 的类似类可以命名为 FootballTeamBuilder,依此类推。

public class HockeyTeamBuilder implements TeamBuilder {

    private Team team;

    public HockeyTeamBuilder(){
        this.team = new Team();
    }

    @Override
    public void buildPlayerList() {
        List players = new ArrayList();
        for(int x = 0; x <= 10; x++){
            players.add(PlayerFactory.getPlayer());
        }
        team.setPlayers(players);
    }

    @Override
    public void buildNewTeam(String teamName) {
        team.setName(teamName);
    }

    @Override
    public void designateTeamCity(String city){
        team.setCity(city);
    }

    public Team getTeam(){
        return this.team;
    }

}

最后,通过调用在其接口中定义的方法来使用构建器创建团队。下面的代码演示了如何使用这个生成器来创建一个团队。您可以在这个菜谱的源代码中使用花名册类来测试这个代码:

public Team createTeam(String teamName, String city){
    TeamBuilder builder = new HockeyTeamBuilder();
    builder.buildNewTeam(teamName);
    builder.designateTeamCity(city);
    builder.buildPlayerList();
    return builder.getTeam();
}

尽管构建器模式的演示相对较短,但它演示了如何隐藏对象的实现细节,从而使对象更容易构建。您不需要知道构建器中的方法实际上做什么;你只需要呼唤他们。

它是如何工作的

构建器模式提供了一种以过程方式生成对象的新实例的方法。它抽象了对象创建的细节,因此创建者不需要做任何特定的工作来生成新的实例。通过将工作分解成一系列步骤,构建器模式允许对象以不同的方式实现其构建器方法。因为对象创建者只能访问构建器方法,所以创建不同的对象类型要容易得多。

有几个类和接口是使用构建器模式所必需的。首先,您需要定义一个类及其不同的属性。正如这个配方的解决方案所展示的,这个类可能遵循 JavaBean 模式(更多细节见配方 5-5)。通过创建 JavaBean,您将能够使用它的 setters 和 getters 来填充对象。接下来,您应该创建一个接口,用于访问您创建的对象的 setters。每个 setter 方法都应该在接口中定义,然后对象本身应该实现该接口。正如在解决方案中所看到的,Team 对象包含以下 setterss,并且每个 setter 都是在 TeamType 接口中定义的:

public void setPlayers(List<Player> players);
public void setName(String name);
public void setCity(String city);

现实生活中,一个团队大概会包含更多的属性。例如,您可能想要设置一个吉祥物和一个主体育场的名称和地址。这个例子中的代码可以被认为是缩写的,因为它演示了一个通用的“团队对象”的创建,而不是向您展示创建一个真实团队的所有代码。因为 Team 类实现了这些在 TeamType 接口中定义的 setters,所以可以调用接口方法来与 Team 类的实际方法进行交互。

在对对象及其接口进行编码之后,需要创建实际的构建器。生成器由一个接口及其实现类组成。首先,您必须定义在构建对象时希望其他类调用的方法。例如,在这个配方的解决方案中,在名为 TeamBuilder 的构建器接口中定义了 buildNewTeam()、designateTeamCity()和 buildPlayerList()方法。当一个类以后想要构建这些对象之一时,它只需要调用这些定义的方法就可以了。接下来,定义一个构建器类实现。实现类将实现在构建器接口中定义的方法,对对象创建者隐藏这些实现的所有细节。在这个配方的解决方案中,构建器类 HockeyTeamBuilder 实现了 TeamBuilder 接口。当一个类想要创建一个新的团队对象时,它只是实例化一个新的构建器类。

TeamBuilder builder = new HockeyTeamBuilder();

为了填充新创建的类对象,在其上调用构建器方法。

builder.buildNewTeam(teamName);
builder.designateTeamCity(city);
builder.buildPlayerList();

使用这种技术为对象提供了一步一步的创建。构建该对象的实现细节对对象创建者是隐藏的。对于不同的构建器实现来说,使用相同的 TeamBuilder 接口来构建不同类型的团队对象是非常容易的。例如,可以编写一个构建器实现来为足球生成团队对象,而另一个实现可以被定义来为棒球生成团队对象。每个团队对象的实现都是不同的。然而,它们都可以实现相同的接口——team builder——并且创建者可以简单地调用构建器方法而不用关心细节。

5-9.通过接口与类交互

问题

您已经创建了一个实现接口或类类型的类。您希望通过调用接口中声明的方法来与该类的方法进行交互,而不是直接使用该类。

解决办法

将同一类型的字段声明为接口。然后,可以将实现接口的类分配给已声明的字段,并调用接口中声明的方法来执行工作。在下面的示例中,一个字段被声明为 TeamType 类型。使用配方 5-8 中的相同类,你可以看到类 Team 实现了 TeamType 接口。以下示例中创建的字段包含对新团队对象的引用。

因为 Team 类实现了 TeamType 接口,所以可以使用该接口中公开的方法:

TeamType team = new Team();
team.setName("Juneau Royals");
team.setCity("Chicago");
System.out.println(team.getFullName());

结果输出:

Juneau Royals – Chicago

它是如何工作的

接口的用处有很多。接口的两个最重要的用例是一致性和抽象。接口定义了一个模型,任何实现接口的类都必须符合这个模型。因此,如果在接口中定义了一个常量,它将自动在类中使用。如果在接口中定义了一个方法,那么这个类必须实现这个方法,除非已经定义了一个默认的实现(见方法 5-7)。接口提供了一种很好的方式让类符合标准。

接口对任何不需要看到的类隐藏不必要的信息。接口中定义的任何方法都是公共的,任何类都可以访问。正如这个配方的解决方案中所演示的,创建了一个对象,并将其声明为接口的类型。示例中的接口 TeamType 只包含团队对象中可用的一小部分方法。因此,对于任何处理已声明为 TeamType 的对象的类来说,唯一可访问的方法是那些在接口中定义的方法。使用此接口类型的类不能访问任何其他方法或常数,也不需要访问。接口是隐藏不需要被其他类使用的逻辑的好方法。另一个很大的副作用是:实现接口的类可以被改变和重新编译,而不会影响使用该接口的代码。然而,如果一个接口被改变,可能会对实现它的任何类产生影响。因此,如果 getFullName()方法实现发生变化,任何针对 TeamType 接口编码的类都不会受到影响,因为接口没有变化。实现将在幕后改变,任何处理接口的类都将开始使用新的实现,而不需要知道。

注意

在某些情况下,现有类的变更会导致代码中断。在使用库时,这种情况更为常见。例如,假设一个类实现了一个用新方法签名更新的接口。实现该接口的所有类现在都必须更新,以包括新方法的实现,为了保持向后兼容性,这在库类中有时是不可能的。这是 Java 8 中包含默认方法的主要原因;更多详情见配方 5-7。

最后,接口有助于提高安全性。它们隐藏了在接口中声明的方法的实现细节,以免任何类使用该接口调用该方法。如前一段所述,如果一个类对 TeamType 接口调用 getFullName()方法,只要结果按预期返回,它就不需要知道该方法的实现细节。

旧的 Enterprise JavaBean (EJB)模型使用接口与执行数据库工作的方法进行交互。这个模型很好地隐藏了其他类不需要的细节和逻辑。其他框架使用类似的模型,通过 Java 接口公开功能。接口的使用已经被证明是一种聪明的软件编码方式,因为它提高了可重用性、灵活性和安全性。

5-10.使类可克隆

问题

您希望一个类能够被另一个类克隆或复制。

解决办法

在要克隆的类中实现可克隆接口;然后调用该对象克隆方法来复制它。下面的代码演示了如何使 Team 类可克隆:

public class Team implements TeamType, Cloneable, Serializable {

    private String name;
    private String city;

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the city
     */
    public String getCity() {
        return city;
    }

    /**
     * @param city the city to set
     */
    public void setCity(String city) {
        this.city = city;
    }

    public String getFullName() {
        return this.name + " - " + this.city;
    }

    /**
     * Overrides Object's clone method to create a deep copy
     *
     * @return
     */
    @Override
    public Team clone() {

        Team obj = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this);
            oos.close();

            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            obj = (Team) ois.readObject();
            ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException cnfe) {
            cnfe.printStackTrace();
        }
        return obj;
    }

    /**
     * Overrides Object's clone method to create a shallow copy
     *
     * @return
     */
    public Team shallowCopyClone() {

        try {
            return (Team) super.clone();
        } catch (CloneNotSupportedException ex) {
            return null;
        }
    }

    @Override
    public boolean equals(Object obj) {

        if (this == obj) {
            return true;
        }
        if (obj instanceof Team) {
            Team other = (Team) obj;
            return other.getName().equals(this.getName())
                    && other.getCity().equals(this.getCity());
        } else {
            return false;
        }

    }
} 

要制作团队对象的深层副本,需要针对该对象调用 clone()方法。要制作对象的浅层副本,必须调用 shallowCopyClone()方法。以下代码演示了这些技术:

Team team1 = new Team();
Team team2 = new Team();

team1.setCity("Boston");
team1.setName("Bandits");

team2.setCity("Chicago");
team2.setName("Wildcats");

Team team3 = team1;
Team team4 = team2.clone();

Team team5 = team1.shallowCopyClone();

System.out.println("Team 3:");
System.out.println(team3.getCity());
System.out.println(team3.getName());

System.out.println("Team 4:");
System.out.println(team4.getCity());
System.out.println(team4.getName());

// Teams move to different cities
team1.setCity("St. Louis");
team2.setCity("Orlando");

System.out.println("Team 3:");
System.out.println(team3.getCity());
System.out.println(team3.getName());

System.out.println("Team 4:");
System.out.println(team4.getCity());
System.out.println(team4.getName());

System.out.println("Team 5:");
System.out.println(team5.getCity());
System.out.println(team5.getName());

if (team1 == team3){
    System.out.println("team1 and team3 are equal");
} else {
    System.out.println("team1 and team3 are NOT equal");
}

if (team1 == team5){
    System.out.println("team1 and team5 are equal");
} else {
    System.out.println("team1 and team5 are NOT equal");
}

这段代码演示了如何克隆一个对象。结果输出如下。

Team 3:
Boston
Bandits
Team 4:
Chicago
Wildcats
Team 3:
St. Louis
Bandits
Team 4:
Chicago
Wildcats
Team 5:
Boston
Bandits
team1 and team3 are equal
team1 and team5 are NOT equal 

它是如何工作的

有两种不同的策略可用于复制对象:浅层副本和深层副本。可以制作一个浅拷贝,它将拷贝该对象,而不拷贝它的任何内容或数据。相反,所有变量都通过引用传递到复制的对象中。创建对象的浅层副本后,原始对象及其副本中的对象引用相同的数据和内存。因此,修改原始对象的内容也会修改复制的对象。默认情况下,对对象调用 super.clone()方法会执行浅层复制。这个菜谱的解决方案中的 shallowCopyClone()方法演示了这种技术。

可以进行的第二种复制称为深度复制,它复制包含所有内容的对象。因此,每个对象都引用内存中不同的空间,修改一个对象不会影响另一个。在这个配方的解决方案中,展示了深层拷贝和浅层拷贝之间的区别。首先,创建团队 1 和团队 2。接下来,用一些值填充它们。然后,team3 对象被设置为与 team1 对象相等,而 team4 对象是 team2 对象的克隆。当 team1 对象中的值发生变化时,它们在 team3 对象中也会发生变化,因为这两个对象的内容指向内存中的同一个空间。这是一个对象浅层拷贝的例子。当 team2 对象中的值发生变化时,它们在 team4 对象中保持不变,因为每个对象都有自己的变量,这些变量引用内存中的不同空间。这是深层拷贝的一个例子。

为了制作对象的精确副本(深层副本),您必须序列化对象,以便可以将其写入磁盘。基本对象类实现了 clone()方法。默认情况下,对象类的 clone()方法是受保护的。为了使一个对象可克隆,它必须实现可克隆接口并覆盖默认的 clone()方法。您可以通过一系列步骤序列化对象来制作对象的深层副本,例如将对象写入输出流,然后通过输入流读回它。这个菜谱的解决方案的 clone()方法中显示的步骤就是这样做的。该对象被写入 ByteArrayOutputStream,然后使用 ByteArrayInputStream 读取。一旦发生这种情况,对象就被序列化,这就创建了深层副本。此配方的解决方案中的 clone()方法已被覆盖,因此它创建了一个深层副本。

一旦遵循了这些步骤,并且对象实现了 Cloneable 并覆盖了默认的 object clone()方法,就可以克隆对象了。为了制作对象的深层副本,只需调用该对象的被覆盖的 clone()方法,如解决方案中所示。如果只是从 clone()方法返回 Object,那么就需要进行类型转换,如下所示:

Team team4 = (Team) team2.clone();

克隆对象并不十分困难,但是很好地理解对象副本之间的差异是很重要的。

5-11.比较对象

问题

您的应用需要能够比较两个或多个对象,以查看它们是否相同。

解决方案 1

若要确定两个对象引用是否指向同一个对象,请使用==和!=运算符。下面的解决方案演示了两个对象引用的比较,以确定它们是否引用同一个对象。

// Compare if two objects contain the same values
Team team1 = new Team();
Team team2 = new Team();

team1.setName("Jokers");
team1.setCity("Crazyville");

team2.setName("Jokers");
team2.setCity("Crazyville");

if (team1 == team2){
    System.out.println("These object references refer to the same object.");
} else {
    System.out.println("These object references do NOT refer to the same object.");
}

// Compare two objects to see if they refer to the same object
Team team3 = team1;
Team team4 = team1;

if (team3 == team4){
    System.out.println("These object references refer to the same object.");
} else {
    System.out.println("These object references do NOT refer to the same object.");
}

运行代码的结果:

These object references do NOT refer to the same object.
These object references refer to the same object. 

解决方案 2

若要确定两个对象是否包含相同的值,请使用 equals()方法。被比较的对象必须实现 equals()和 hashCode(),这样这个解决方案才能正常工作。下面是覆盖这两个方法的 Team 类的代码:

public class Team implements TeamType, Cloneable {

    private List<Player> players;
    private String name;
    private String city;
    // Used by the hashCode method for performance reasons
    private volatile int cachedHashCode = 0;

    /**
     * @return the players
     */
    public List<Player> getPlayers() {
        return players;
    }

    /**
     * @param players the players to set
     */
    public void setPlayers(List<Player> players) {
        this.players = players;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the city
     */
    public String getCity() {
        return city;
    }

    /**
     * @param city the city to set
     */
    public void setCity(String city) {
        this.city = city;
    }

    public String getFullName() {
        return this.name + " - " + this.city;
} 

    /**
     * Overrides Object's clone method
     *
     * @return
     */
    public Object clone() {

        try {
            return super.clone();
        } catch (CloneNotSupportedException ex) {
            return null;
        }
    }

    @Override
    public boolean equals(Object obj) {

        if (this == obj) {
            return true;
        }
        if (obj instanceof Team) {
            Team other = (Team) obj;
            return other.getName().equals(this.getName())
&& other.getCity().equals(this.getCity())
&& other.getPlayers().equals(this.getPlayers());
        } else {
            return false;
        }

} 

@Override
    public int hashCode() {
        int hashCode = cachedHashCode;
        if (hashCode == 0) {
            String concatStrings = name + city;
            if (players.size() > 0) {
                for (Player player : players) {
                    concatStrings = concatStrings
                            + player.getFirstName()
                            + player.getLastName()
                            + player.getPosition()
                            + String.valueOf(player.getStatus());

                }
            }
            hashCode = concatStrings.hashCode();
        }
        return hashCode;
    }
} 

下面的解决方案演示了包含相同值的两个对象的比较。

// Compare if two objects contain the same values
Team team1 = new Team();
Team team2 = new Team();

// Build Player List
Player newPlayer = new Player("Josh", "Juneau");
playerList.add(0, newPlayer);
newPlayer = new Player("Jonathan", "Gennick");
playerList.add(1, newPlayer);
newPlayer = new Player("Joe", "Blow");
playerList.add(1, newPlayer);
newPlayer = new Player("John", "Smith");
playerList.add(1, newPlayer);
 newPlayer = new Player("Paul", "Bunyan");
playerList.add(1, newPlayer);

team1.setName("Jokers");
team1.setCity("Crazyville");
team1.setPlayers(playerList);

team2.setName("Jokers");
team2.setCity("Crazyville");
team2.setPlayers(playerList);

if (team1.equals(team2)){
    System.out.println("These object references contain the same values.");
} else {
    System.out.println("These object references do NOT contain the same values.");
}

运行这段代码的结果是:

These object references do NOT refer to the same object.
These object references contain the same values.
These object references refer to the same object.

它是如何工作的

比较运算符(==)可用于确定两个对象是否相等。这种相等不属于对象值,而是属于对象引用。通常应用更关心对象的值;在这种情况下,equals()方法是首选,因为它比较的是对象中包含的值,而不是对象引用。

比较运算符查看对象引用,并确定它是否指向与要比较的对象引用相同的对象。如果两个对象相等,将返回布尔值 true 结果;否则,将返回布尔假结果。在解决方案 1 中,team1 对象引用和 team2 对象引用之间的第一次比较返回 false 值,因为这两个对象在内存中是分开的,即使它们包含相同的值。在解决方案 1 中,team3 对象引用和 team4 对象引用之间的第二次比较返回 true 值,因为这两个引用都引用了 team1 对象。

equals()方法可用于测试两个对象是否包含相同的值。为了使用 equals()方法进行比较,被比较的对象应该覆盖 object 类 equals()和 hashCode()方法。equals()方法应该实现与包含在对象中的值进行比较,从而产生真实的比较结果。以下代码是一个被覆盖的 equals()方法的示例,该方法已被放入 Team 对象中:

@Override
public boolean equals(Object obj) {

    if (this == obj) {
        return true;
    }
    if (obj instanceof Team) {
        Team other = (Team) obj;
        return other.getName().equals(this.getName())
&& other.getCity().equals(this.getCity())
&& other.getPlayers().equals(this.getPlayers());
    } else {
        return false;
    }

}

正如您所看到的,被覆盖的 equals()方法首先检查作为参数传递的对象是否引用了与它进行比较的对象相同的对象。如果是,则返回真结果。如果两个对象没有引用内存中的同一个对象,equals()方法会检查这两个字段是否相等。在这种情况下,任何两个在 name 和 city 字段中包含相同值的团队对象都将被视为相等。一旦 equals()方法被覆盖,就可以执行这两个对象的比较,如该配方的解决方案 2 所示。

hashCode()方法返回一个 int 值,该值必须始终返回同一个整数。有很多方法可以计算对象的 hashCode。在网上搜索一下这个话题,你会发现各种各样的技巧。实现 hashCode()方法的一个最基本的方法是将所有对象的变量连接成字符串格式,然后返回结果字符串的 hashCode()。缓存 hashCode 的值供以后使用是一个好主意,因为初始计算可能需要一些时间。解决方案 2 中的 hashCode()方法演示了这种策略。

考虑到有多种方法可以比较 Java 对象,比较 Java 对象可能会变得令人困惑。如果要对对象标识执行比较,请使用比较(==)运算符。但是,如果您想要比较对象中的值,或者对象的状态,那么 equals()方法是一个不错的选择。

5-12.扩展类的功能

问题

你的一个应用包含了一个类,你想用它作为另一个类的基础。您希望您的新类包含该基类的相同功能,但还包含其他功能。

解决办法

通过使用 extends 关键字后跟要扩展的类的名称来扩展基类的功能。下面的示例显示了两个类。第一个类名为 HockeyStick,表示一个曲棍球棒对象。它将由名为 WoodenStick 的第二个类扩展。通过这样做,WoodenStick 类将继承 HockeyStick 中包含的所有属性和功能,私有变量和具有默认访问级别的变量除外。WoodenStick 类成为 HockeyStick 的子类。首先,让我们看一下 HockeyStick 类,它包含标准曲棍球棒的基本属性:

public class HockeyStick {

    private int length;
    private boolean curved;
    private String material;

    public HockeyStick(int length, boolean curved, String material){
        this.length = length;
        this.curved = curved;
        this.material = material;
    }

     /**
     * @return the length
     */
    public int getLength() {
        return length;
    }

    /**
     * @param length the length to set
     */
    public void setLength(int length) {
        this.length = length;
    }

    /**
     * @return the curved
     */
    public boolean isCurved() {
        return curved;
    }

    /**
     * @param curved the curved to set
     */
    public void setCurved(boolean curved) {
        this.curved = curved;
    }

    /**
     * @return the material
     */
    public String getMaterial() {
        return material;
    }

    /**
     * @param material the material to set
     */
    public void setMaterial(String material) {
        this.material = material;
    }

}

接下来,看看 HockeyStick 的子类:一个名为 WoodenStick 的类。

public class WoodenStick extends HockeyStick {

    private static final String material = "WOOD";
    private int lie;
    private int flex;

    public WoodenStick(int length, boolean isCurved){
        super(length, isCurved, material);
    }

    public WoodenStick(int length, boolean isCurved, int lie, int flex){
        super(length, isCurved, material);
        this.lie = lie;
        this.flex = flex;
    }

    /**
     * @return the lie
     */
    public int getLie() {
        return lie;
    }

    /**
     * @param lie the lie to set
     */
    public void setLie(int lie) {
        this.lie = lie;
    }

    /**
     * @return the flex
     */
    public int getFlex() {
        return flex;
    }

    /**
     * @param flex the flex to set
     */
    public void setFlex(int flex) {
        this.flex = flex;
    }
}
注意

在这个例子中,我们假设可能有不止一种类型的 HockeyStick。在这种情况下,我们扩展 HockeyStick 来创建一个 WoodenStick,但是我们也可以扩展 HockeyStick 来创建其他类型的 HockeyStick,比如 AluminumStick 或 GraphiteStick。

它是如何工作的

对象继承是任何面向对象语言的基本技术。从基类继承增加了价值,因为它允许代码在多个地方重用。这有助于使代码管理更加容易。如果在基类中进行了更改,它将自动在子类中继承。另一方面,如果您的应用中分散着重复的功能,那么一个微小的更改就意味着您必须在许多地方更改代码。对象继承也使得为一个或多个子类指定一个基类变得容易,这样每个类可以包含相似的字段和功能。

Java 语言只允许一个类扩展另一个类。这在概念上不同于包含多重继承的其他语言,如 C++。虽然有些人认为单个类继承是语言的障碍,但它是为了增加语言的安全性和易用性而设计的。当一个子类包含多个超类时,混乱就会接踵而至。

5-13.为要扩展的类定义模板

问题

您希望定义一个模板,用于生成包含类似功能的对象。

解决办法

定义一个抽象类,其中包含可以在其他类中使用的字段和功能。抽象类还可以包含未实现的方法,称为抽象方法,需要由抽象类的子类实现。下面的示例演示了抽象类的概念。示例中的抽象类表示一个团队日程,它包括一些基本的字段声明和每个团队的日程都需要使用的功能。然后由 TeamSchedule 类扩展 Schedule 类,它将用于为每个团队实现特定的功能。首先,让我们看看抽象的 Schedule 类:

public abstract class Schedule {

    public String scheduleYear;
    public String teamName;

    public List<Team> teams;

    public Map<Team, LocalDate> gameMap;

    public Schedule(){}

    public Schedule(String teamName){
        this.teamName = teamName;
    }

    abstract void calculateDaysPlayed(int month);

}

接下来,TeamSchedule 扩展了抽象类的功能。

public class TeamSchedule extends Schedule {

    public TeamSchedule(String teamName) {
        super(teamName);
    }

    @Override
    void calculateDaysPlayed(int month) {
        int totalGamesPlayedInMonth = 0;
        for (Map.Entry<Team, LocalDate> entry : gameMap.entrySet()) {
            if (entry.getKey().equals(teamName)
                    && entry.getValue().getMonth().equals(month)) {
                totalGamesPlayedInMonth++;
            }
        }
        System.out.println("Games played in specified month: " + totalGamesPlayedInMonth);
    }

}

如您所见,TeamSchedule 类可以使用抽象 Schedule 类中包含的所有字段和方法。它还实现了包含在 Schedule 类中的抽象方法。

它是如何工作的

抽象类就是这样标记的,它们包含可以在子类中使用的字段声明和方法。它们与常规类的不同之处在于它们可以包含抽象方法,抽象方法是没有实现的方法声明。这个配方的解决方案包含一个名为 calculateDaysPlayed()的抽象方法。抽象类可能包含也可能不包含抽象方法。它们可以包含字段和完全实现的方法。抽象类不能被实例化;其他类只能扩展它们。当一个类扩展一个抽象类时,它获得该抽象类的所有字段和功能。然而,在抽象类中声明的任何抽象方法都必须由子类实现。

您可能想知道为什么抽象类不仅仅包含方法的实现,这样它的所有子类都可以使用它。如果你思考一下这个概念,它就非常有意义。一种类型的对象执行的任务可能与另一种不同。使用抽象方法会强制扩展抽象类的类实现它,但它允许自定义实现方式的能力。

5-14.增加类封装

问题

您的一个类需要使用另一个类的功能。但是,没有其他类需要使用相同的功能。您希望生成一个只能由需要它的类使用的实现,同时将代码放在一个逻辑位置,而不是创建一个包含这一附加功能的单独的类。

解决办法

在需要其功能的类中创建一个内部类。

import java.util.ArrayList;
import java.util.List;

/**
 * Inner class example. This example demonstrates how a team object could be
 * built using an inner class object.
 *
 * @author juneau
 */
public class TeamInner {

    private Player player;
    private List<Player> playerList;
    private int size = 4;

    /**
     * Inner class representing a Player object
     */
    class Player {

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

        public Player() {
        }

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

        protected String playerStatus() {
            String returnValue = null;

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

            return returnValue;
        }

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

        /**
         * @return the firstName
         */
        public String getFirstName() {
            return firstName;
        }

        /**
         * @param firstName the firstName to set
         */
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        /**
         * @return the lastName
         */
        public String getLastName() {
            return lastName;
} 

        /**
         * @param lastName the lastName to set
         */
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        /**
         * @return the position
         */
        public String getPosition() {
            return position;
        }

        /**
         * @param position the position to set
         */
        public void setPosition(String position) {
            this.position = position;
        }

        /**
         * @return the status
         */
        public int getStatus() {
            return status;
        }

        /**
         * @param status the status to set
         */
        public void setStatus(int status) {
            this.status = status;
        }

        @Override
        public String toString(){
            return this.firstName + " " + this.lastName + " - "+
                   this.position + ": " + this.playerStatus();
        }
    }

    /**
     * Inner class that constructs the Player objects and adds them to an array
     * that was declared in the outer class;
     */
    public TeamInner() {

        final int ACTIVE = 0;

        // In reality, this would probably read records from a database using
        // a loop...but for this example we will manually enter the player data.
        playerList = new ArrayList();
        playerList.add(constructPlayer("Josh", "Juneau", "Right Wing", ACTIVE));
        playerList.add(constructPlayer("Joe", "Blow", "Left Wing", ACTIVE));
        playerList.add(constructPlayer("John", "Smith", "Center", ACTIVE));
        playerList.add(constructPlayer("Bob","Coder", "Defense", ACTIVE));
        playerList.add(constructPlayer("Jonathan", "Gennick", "Goalie", ACTIVE));
    }

    public Player constructPlayer(String first, String last, String position, int status){
            Player player = new Player();
            player.firstName = first;
            player.lastName = last;
            player.position = position;
            player.status = status;
            return player;
    }

    public List<Player> getPlayerList() {
        return this.playerList;
    }

    public static void main(String[] args) {
TeamInner inner = new TeamInner();
        System.out.println("Team Roster");
        System.out.println("===========");
for(Player player:inner.getPlayerList()){
            System.out.println(player.playerString());
        }
    }
} 

运行这段代码的结果是一个团队成员的列表。

Team Roster
===========
Josh Juneau - Right Wing
Joe Blow - Left Wing
John Smith - Center
Bob Coder - Defense
Jonathan Gennick - Goalie

它是如何工作的

有时将功能封装在单个类中很重要。其他时候,为只在另一个类中使用的功能包含一个单独的类是没有意义的。假设您正在开发一个 GUI,您需要使用一个类来支持一个按钮的功能。如果按钮类中没有可重用的代码,那么创建一个单独的类并公开该功能供其他类使用是没有意义的。相反,将该类封装在需要该功能的类中是有意义的。这一理念是内部类(也称为嵌套类)的一个用例。

内部类是包含在另一个类中的类。内部类可以像任何其他类一样被公开、私有或保护。它可以包含与普通类相同的功能;唯一的区别是内部类包含在封闭类中,也称为外部类。这个配方的解决方案演示了这种技术。TeamInner 类包含一个名为 Player 的内部类。Player 类是一个表示 Player 对象的 JavaBean 类。如您所见,Player 对象能够从其包含的类继承功能,包括其私有字段。这是因为内部类包含对外部类的隐式引用。它也可以由包含它的 TeamInner 类访问,如 constructPlayer()方法中所示:

public Player constructPlayer(String first, String last, String position, int status){
            Player player = new Player();
            player.firstName = first;
            player.lastName = last;
            player.position = position;
            player.status = status;
            return player;
    }

外部类可以根据需要多次实例化内部类。在这个例子中,constructPlayer()方法可以被调用任意次,实例化内部类的一个新实例。但是,当实例化外部类时,不会实例化内部类的任何实例。类似地,当外部类不再使用时,所有内部类实例也被销毁。

内部类可以通过引用外部类和它想要调用的方法来引用外部类方法。下面一行代码演示了这样一个引用,它使用了这个配方的解决方案中表示的相同对象。假设玩家类需要从外部类获得玩家列表;您应该编写类似下面的内容:

TeamInner.this.getPlayerList();

虽然不经常使用,但外部类以外的类可以通过使用以下语法获得对公共内部类的访问:

TeamInner outerClass = new TeamInner();
outerClass.player = outerClass.new Player();

静态内部类有点不同,因为它们不能直接引用其封闭类的任何实例变量或方法。下面是一个静态内部类的例子。

public class StaticInnerExample {

    static String hello = "Hello";

    public static void sayHello(){
        System.out.println(hello);
    }

    static class InnerExample {
        String goodBye = "Good Bye";

        public void sayGoodBye(){
            System.out.println(this.goodBye);
        }
    }

    public static void main (String[] args){
        StaticInnerExample.sayHello();
        StaticInnerExample.InnerExample inner =
                new StaticInnerExample.InnerExample();
        inner.sayGoodBye();
    }
}

内部类有助于提供逻辑封装。此外,它们允许私有字段的继承,这在使用标准类时是不可能的。

摘要

Java 是一种面向对象的语言。为了利用这种语言的能力,人们必须学会如何精通面向对象。本章讲述了诸如类创建和访问修饰符之类的基础知识。它还涵盖了封装、接口和配方,以帮助开发人员利用面向对象的强大功能。

六、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 发布时引入的最重要的更新之一,用于处理集合。

八、输入和输出

在应用中,经常需要获得和操作 I/O 终端。在今天的操作系统中,这通常意味着文件访问和网络连接。在以前的版本中,为了保持普遍的兼容性,Java 在采用良好的文件和网络框架方面进展缓慢。坚持“一次写入,随处读取”的原则,许多原始文件 I/O 和网络连接需要简单和通用。自从 Java 7 发布以来,开发人员一直在利用更好的 I/O API。

这些年来,文件和网络 I/O 已经发展成为一个处理文件、网络可伸缩性和易用性更好的框架。从网络输入输出版本 2 API (NIO.2)开始,Java 具有了监控文件夹、访问依赖于操作系统的方法以及创建可伸缩的异步网络套接字的能力。这是对处理输入和输出流以及序列化(和反序列化)对象信息的健壮库的补充。

在这一章中,我们将介绍演示不同输入和输出过程的配方。您将学习文件序列化、通过网络发送文件、文件操作等等。阅读完本章的食谱后,你将具备开发包含复杂输入和输出任务的应用的能力。

流和装饰模式

I/O 流是大多数 Java I/O 的基础,并且包括了几乎任何场合都可以使用的大量现成流,但是如果没有提供一些上下文,使用起来会非常混乱。流(像河流一样)表示数据的流入/流出。这么想吧。当您键入时,您创建了系统接收的字符流(输入流)。当系统产生声音时,它将声音发送到扬声器(输出流)。系统可能整天都在接收击键和发送声音,因此数据流可能在处理数据,也可能在等待更多的数据。

当一个流没有接收到任何数据时,它会等待(没有其他事情可做,对吗?).数据一进来,流就开始处理这些数据。然后,该流停止并等待下一个数据项的到来。这样一直持续下去,直到这条众所周知的河流变干(溪流被封闭)。

像河流一样,流可以相互连接(这就是装饰模式)。对于本章的内容,主要有两个您关心的输入流。其中一个是文件输入流,另一个是网络套接字输入流。这两个流是 I/O 程序的数据源。还有它们对应的输出流:文件输出流和网络套接字输出流(多有创意啊,不是吗?).就像水管工一样,你可以把它们连在一起,创造出新的东西。例如,您可以将一个文件输入流与一个网络输出流焊接在一起,通过网络套接字发送文件内容。或者,您可以反过来将网络输入流(传入的数据)连接到文件输出流(写入磁盘的数据)。在 I/O 术语中,输入流被称为,而输出流被称为

还有其他输入和输出流可以粘合在一起。比如有 BufferedInputStream,允许你成块读取数据(比一个字节一个字节地读取效率更高),DataOutputStream 允许你把 Java 原语写到一个输出流中(而不是只写字节)。最有用的流之一是 ObjectInputStream 和 ObjectOutputStream 对,它允许你序列化/反序列化对象(见方法 8-1)。

decorator 模式允许您不断地一起提取流,以获得许多不同的效果。这种设计的美妙之处在于,你实际上可以创建一个流,它可以接受任何输入并产生任何输出,然后可以与其他流一起抛出。

8-1.序列化 Java 对象

问题

您需要序列化一个类(保存该类的内容),以便以后可以恢复它。

解决办法

Java 实现了内置的序列化机制。您可以通过 ObjectOutputStream 类访问该机制。在以下示例中,saveSettings()方法使用 ObjectOutputStream 来序列化 Settings 对象,为将该对象写入磁盘做准备:

public class Ch_8_1_SerializeExample {
    public static void main(String[] args) {
        Ch_8_1_SerializeExample example = new Ch_8_1_SerializeExample();
        example.start();
    }

    private void start() {
        ProgramSettings settings = new ProgramSettings(new Point(10,10),  
                                                         new Dimension(300,200),   Color.blue,
                                                       "The title of the application" );
        saveSettings(settings,"settings.bin");
        ProgramSettings loadedSettings = loadSettings("settings.bin");
        if(loadedSettings != null)
            System.out.println("Are settings are equal? :"+loadedSettings.equals(settings));

    }

    private void saveSettings(ProgramSettings settings, String filename) {
        try {
            FileOutputStream fos = new FileOutputStream(filename);
            try (ObjectOutputStream oos = new ObjectOutputStream(fos)) {
                oos.writeObject(settings);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private ProgramSettings loadSettings(String filename) {
        try {
            FileInputStream fis = new FileInputStream(filename);
            ObjectInputStream ois = new ObjectInputStream(fis);
            return (ProgramSettings) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

它是如何工作的

Java 支持序列化,这是获取一个对象并创建一个字节表示的能力,该字节表示可用于以后恢复该对象。通过使用内部序列化机制,序列化对象的大部分设置都被处理了。Java 将把对象的属性转换成字节流,然后可以保存到文件中或通过网络传输。

注意

最初的 Java 序列化框架使用反射来序列化对象,所以如果序列化/反序列化太多,可能会有问题。有许多开源框架根据您的需要提供不同的权衡(速度与大小与易用性)。参见github.com/eishay/jvm-serializers/wiki/

对于一个可序列化的类,它需要实现 serializable 接口,这是一个标记接口:它没有任何方法,而是告诉序列化机制你已经允许你的类被序列化。虽然从一开始就不明显,但序列化公开了类的所有内部工作方式(包括受保护的和私有的成员),所以如果您想对核发射的授权代码保密,您可能希望使包含此类信息的任何类都不可序列化。

该类的所有属性(也称为成员、变量或字段)都必须是可序列化的(和/或瞬态的,这一点我们稍后会谈到)。所有原语—int、long、double 和 float(以及它们的包装类)—以及 String 类,在设计上都是可序列化的。其他 Java 类可以根据具体情况进行序列化。例如,不能序列化任何 Swing 组件(像 JButton 或 JSpinner),也不能序列化 File 对象,但是可以序列化 Color 类(更准确地说是 awt.color)。

作为一个设计原则,您不希望序列化您的主类,而是希望创建只包含您希望序列化的属性的类。它将省去调试中的许多麻烦,因为序列化变得非常普遍。如果您将一个主要类标记为 serializable(实现 serializable ),并且该类包含许多其他属性,那么您也需要将这些类声明为 Serializable。如果您的 Java 类继承自另一个类,父类也应该是可序列化的。在父类不可序列化的情况下,父类的属性将不会被序列化。

如果您想将属性标记为不可序列化,您可以将其标记为瞬态。瞬态属性告诉 Java 编译器,您对保存/加载属性值不感兴趣,因此它将被忽略。有些属性很可能是瞬态的,比如缓存计算,或者总是实例化为相同值的日期格式化程序。

凭借序列化框架,静态属性是不可序列化的;静态类也不是。原因是静态类不能被实例化,尽管公共静态内部类可以被实例化。因此,如果您保存然后同时加载静态类,您将加载静态类的另一个副本,从而抛出 JVM 进行循环。

Java 序列化机制在幕后工作,转换和遍历类中标记为可序列化的每个对象。如果应用包含对象中的对象,甚至可能包含交叉引用的对象,序列化框架将解析这些对象,并且只存储任何对象的一个副本。然后每个属性被转换成 byte[]表示。字节数组的格式包括实际的类名(例如:com . somewhere . over . the . rainbow . preferences . user preferences),后面是属性的编码(这反过来可以编码另一个对象类及其属性等)。等等。,无限期

出于好奇,如果您查看生成的文件(即使在文本编辑器中),您可以看到类名几乎是文件的第一部分。

注意

序列化非常脆弱。默认情况下,序列化框架生成一个流唯一标识符(SUID) ,它捕获关于类中出现了什么字段、它们的种类(公共/受保护)以及什么是瞬态的等信息。即使是对类的细微修改(例如,将 int 改为 long 属性)也会产生一个新的 SUID。用以前的 SUID 保存的类不能在新的 SUID 上反序列化。这样做是为了保护序列化/反序列化机制,同时也保护设计者。

您实际上可以告诉 Java 类使用特定的 SUID。这将允许您序列化类,修改它们,然后反序列化原始类,同时实现一些向后兼容性。您遇到的危险是反序列化必须是向后兼容的。重命名或移除字段将在反序列化类时生成异常。如果在 Serializable 类上指定自己的串行 Serializable,请确保每次更改该类时都进行一些向后兼容性的单元测试。一般来说,可以在下面的地方找到保持类向后兼容的更改:docs . Oracle . com/javase/9/docs/platform/serialization/spec/serial-arch . html

由于序列化的本质,不要期望在反序列化对象时调用构造函数。如果在构造函数中有对象正常工作所需的初始化代码,则可能需要从构造函数中重构代码,以允许在构造后正确执行。原因是在反序列化过程中,反序列化的对象是在内部“还原”的(不是创建的),不调用构造函数。

8-2.更有效地序列化 Java 对象

问题

您希望序列化一个类,但希望使输出比通过内置序列化方法生成的产品更有效或更小。

解决办法

通过使对象实现可外部化的接口,可以指示 Java 虚拟机使用自定义的序列化/反序列化机制,如下例中的 readExternal/writeExternal 方法所提供的那样。

public class ExternalizableProgramSettings implements Externalizable {
    private Point locationOnScreen;
    private Dimension frameSize;
    private Color defaultFontColor;
    private String title;

    // Empty constructor, required for Externalizable implementors
    public ExternalizableProgramSettings() {

    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(locationOnScreen.x);
        out.writeInt(locationOnScreen.y);
        out.writeInt(frameSize.width);
        out.writeInt(frameSize.height);
        out.writeInt(defaultFontColor.getRGB());
        out.writeUTF(title);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        locationOnScreen = new Point(in.readInt(), in.readInt());
        frameSize = new Dimension(in.readInt(), in.readInt());
        defaultFontColor = new Color(in.readInt());
        title = in.readUTF();
    }
// getters and setters omitted for brevity
}

它是如何工作的

Java 序列化框架为您提供了指定序列化对象的实现的能力。因此,它需要实现可外部化的接口来代替可序列化的接口。可外部化的接口包含两个方法:writeExternal(ObjectOutput out)和 readExternal(ObjectInput in)。通过实现这些方法,你告诉框架如何编码/解码你的对象。

writeExternal()方法将 ObjectOutput 对象作为参数传入。然后,该对象将允许您为序列化编写自己的编码。ObjectOutput 包含表 8-1 中列出的方法。

表 8-1。对象输出方法
|

ObjectOutput

|

对象输出

|

描述

|
| --- | --- | --- |
| writeBoolean (boolean v) | booleanreadBoolean() | 读/写布尔原语。 |
| writeByte(int v) | intreadByte() | 读取/写入一个字节。注意:Java 没有字节原语,所以使用 int 作为参数,但是只写最低有效字节。 |
| writeShort(int v) | intreadShort() | 读取/写入两个字节。注意:只有两个最低有效字节会被写入。 |
| writeChar(int v) | intreadChar() | 以 char 形式读/写两个字节(顺序与 writeShort 相反)。 |
| writeInt (int v) | intreadInt() | 读/写一个整数。 |
| writeLong(长 v) | intreadLong() | 读/写 long 类型。 |
| writeDouble(双 v) | 双倍读数 | 读/写一个 double。 |

您可能选择实现可外部化接口而不是可序列化接口的一个原因是,Java 的默认序列化效率非常低。因为 Java 序列化框架需要确保每个对象(和依赖对象)都被序列化,所以它甚至会编写具有默认值或者可能为空和/或 null 的对象。实现外部化接口还提供了对如何序列化类的更细粒度的控制。在我们的例子中,可序列化版本创建了 439 字节的设置,而可外部化版本只有 103 字节!

注意

实现外部化接口的类必须包含一个空的(无参数)构造函数。

8-3.将 Java 对象序列化为 XML

问题

尽管您喜欢序列化框架,但您希望创建至少是跨语言兼容的(或人类可读的)东西。您希望使用 XML 保存和加载您的对象。

解决办法

在此示例中,XMLEncoder 对象用于对 Settings 对象进行编码,该对象包含程序设置信息并将其写入 settings.xml 文件。XMLDecoder 获取 settings.xml 文件并将其作为流读取,对 settings 对象进行解码。文件系统用于获得对机器文件系统的访问;FileOutputStream 用于将文件写入系统;FileInputStream 用于从文件系统中的文件获取输入字节。在本例中,这三个文件对象用于创建新的 XML 文件,并读取它们进行处理。

//Encoding
FileSystem fileSystem = FileSystems.getDefault();
try (FileOutputStream fos = new FileOutputStream("settings.xml"); XMLEncoder encoder =
       new XMLEncoder(fos)) {
    encoder.setExceptionListener((Exception e) -> {
        System.out.println("Exception! :"+e.toString());
    });
    encoder.writeObject(settings);
}

// Decoding
try (FileInputStream fis = new FileInputStream("settings.xml"); XMLDecoder decoder =
       new XMLDecoder(fis)) {
    ProgramSettings decodedSettings = (ProgramSettings) decoder.readObject();
    System.out.println("Is same? "+settings.equals(decodedSettings));
}

Path file= fileSystem.getPath("settings.xml");
List<String> xmlLines = Files.readAllLines(file, Charset.defaultCharset());
xmlLines.stream().forEach((line) -> {
    System.out.println(line);
});

它是如何工作的

XMLEncoder 和 XMLDecoder 与序列化框架一样,使用反射来确定要写入哪些字段,但不是将字段写成二进制,而是写成 XML。要编码的对象不需要是可序列化的,但是它们需要遵循 Java Beans 规范。

Java Bean 是符合以下约定的任何对象的名称:

  • 该对象包含一个公共空(无参数)构造函数。

  • 该对象包含每个名为 get{Property}()和 set{Property}()的受保护/私有属性的公共 getters 和 setters。

XMLEncoder 和 XMLDecoder 将只编码/解码具有公共访问器(get{property},set{property})的 Bean 的属性,因此任何私有且没有访问器的属性都不会被编码/解码。

小费

在编码/解码时注册一个异常监听器是一个好主意。

XmlEncoder 创建一个正在序列化的类的新实例(记住它们必须是 Java Beans,所以它们必须有一个空的无参数构造函数),然后确定哪些属性是可访问的(通过 get{property},set{property})。并且如果新实例化的类的属性包含与原始类的属性相同的值(即,具有相同的默认值),则 XmlEncoder 不写该属性。换句话说,如果一个属性的默认值没有改变,XmlEncoder 不会把它写出来。这提供了在不同版本之间改变“缺省”值的灵活性。例如,如果在对对象进行编码时某个属性的默认值为 2,并且后来在默认属性从 2 更改为 4 后进行了解码,则解码后的对象将包含新的默认属性 4(这可能不正确)。

XMLEncoder 还跟踪引用。如果一个对象在对象图中出现不止一次(例如,一个对象在主类的映射中,但也作为 DefaultValue 属性出现),那么 xmlEncoder 将只对它编码一次,并通过在 XML 中放置一个链接来链接一个引用。XMLEncoder/XMLDecoder 比序列化框架宽容得多。解码时,如果属性类型被更改,或者被删除/添加/移动/重命名,解码将“尽可能多地”解码,同时跳过无法解码的属性。

建议不要持久化您的主类(即使 XMLEncoder 更宽容),而是创建简单的、保存基本信息的特殊对象,这些对象本身不执行许多任务。

8-4.创建套接字连接并通过网络发送可序列化的对象

问题

您需要打开一个网络连接,并从中发送/接收对象。

解决办法

使用 Java 新的输入输出 API 版本 2 (NIO.2)来发送和接收对象。以下解决方案利用了非阻塞套接字的 NIO.2 特性(通过使用未来任务):

public class Ch_8_4_AsyncChannel {
    private AsynchronousSocketChannel clientWorker;

    InetSocketAddress hostAddress;

    public Ch_8_4_AsyncChannel() {
    }

    private void start() throws IOException, ExecutionException, TimeoutException, InterruptedException {
        hostAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 2583);

        Thread serverThread = new Thread(() -> {
            serverStart();
        });

        serverThread.start();

        Thread clientThread = new Thread(() -> {
            clientStart();
        });
        clientThread.start();

    }

    private void clientStart() {
        try {
            try (AsynchronousSocketChannel clientSocketChannel = AsynchronousSocketChannel.open()) {
                Future<Void> connectFuture = clientSocketChannel.connect(hostAddress);
                connectFuture.get();            // Wait until connection is done.
                OutputStream os = Channels.newOutputStream(clientSocketChannel);
                try (ObjectOutputStream oos = new ObjectOutputStream(os)) {
                    for (int i = 0; i < 5; i++) {
                        oos.writeObject("Look at me " + i);
                        Thread.sleep(1000);
                    }
                    oos.writeObject("EOF");
                }
            }
        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

    }

    private void serverStart() {
        try {
            AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open().bind(hostAddress);
            Future<AsynchronousSocketChannel> serverFuture  = serverSocketChannel.accept();
            final AsynchronousSocketChannel clientSocket = serverFuture.get();
            System.out.println("Connected!");
            if ((clientSocket != null) && (clientSocket.isOpen())) {
                try (InputStream connectionInputStream = Channels.newInputStream(clientSocket)) {
                    ObjectInputStream ois = null;
                    ois = new ObjectInputStream(connectionInputStream);
                    while (true) {
                        Object object = ois.readObject();
                        if (object.equals("EOF")) {
                            clientSocket.close();
                            break;
                        }
                        System.out.println("Received :" + object);
                    }
                    ois.close();
                }
            }

        } catch (IOException | InterruptedException | ExecutionException | ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws IOException, ExecutionException, TimeoutException, InterruptedException {
        Ch_8_4_AsyncChannel example = new Ch_8_4_AsyncChannel();
        example.start();
    }
}

它是如何工作的

基本上,套接字需要类型、IP 地址和端口。虽然套接字文学已经占据了整本书,但其主要思想非常简单。像邮局一样,套接字通信依赖于地址。这些地址用于传送数据。在本例中,我们选择了回送(运行程序的同一台计算机)地址(127.0.0.1),并选择了一个随机端口号(2583)。

新 NIO.2 的优势在于它本质上是异步的。通过使用异步调用,您可以扩展您的应用,而不必为每个连接创建数千个线程。在我们的例子中,我们接受异步调用并等待连接,为了这个例子,实际上使它成为单线程的,但是不要让这阻止你用更多的异步调用来增强这个例子。(查看本书多线程部分的方法。)

对于要连接的客户端,它需要一个套接字通道。NIO.2 API 允许创建异步套接字通道。一旦创建了套接字通道,它将需要一个地址来连接。socketChannel.connect()操作不会阻塞;相反,它返回一个 Future 对象(这与传统的 NIO 不同,在传统的 NIO 中,调用 socketChannel.connect()将一直阻塞,直到建立连接为止)。Future 对象允许 Java 程序继续它正在做的事情,并简单地查询已提交任务的状态。打个比方,你不再在前门等你的邮件到达,而是去做其他事情,定期“检查”邮件是否已经到达。Future 对象有 isDone()和 isCancelled()这样的方法,让您知道任务是完成了还是取消了。它还有 get()方法,允许您实际等待任务完成。在我们的示例中,我们使用 Future.get()来等待客户端连接的建立。

一旦建立了连接,我们就使用 Channels.newOutputStream()创建一个输出流来发送信息。使用装饰器模式,我们用 ObjectOutputStream 装饰 outputStream,最终通过套接字发送对象。

服务器代码稍微复杂一点。服务器套接字连接允许出现多个连接,因此它们用于监视或接收连接,而不是启动连接。因此,服务器通常异步等待连接。

服务器首先建立它监听的地址(127.0.0.1:2583)并接受连接。对 serverSocketChannel.accept()的调用返回另一个 Future 对象,该对象将为您提供如何处理传入连接的灵活性。在我们的例子中,服务器连接简单地调用 Future.get(),它将阻塞(停止程序的执行)直到连接被接受。

服务器获取套接字通道后,它通过调用 Channels.newInputStream(socket)创建一个 inputStream,然后用 ObjectInputStream 包装该输入流。然后,服务器继续循环并读取来自 ObjectInputStream 的每个对象。如果对象 received 的 toString()方法等于 EOF,服务器停止循环,连接关闭。

注意

使用 ObjectOutputStream 和 ObjectInputStream 发送和接收大量对象会导致内存泄漏。为了提高效率,ObjectOutputStream 保留了已发送对象的副本。如果要再次发送同一对象,ObjectOutputStream 和 ObjectInputStream 将不会再次发送同一对象,而是发送以前发送的对象 ID。这种行为或者只发送对象 ID 而不是整个对象会引发两个问题。

第一个问题是,当通过网络发送时,被就地更改(可变)的对象将不会在接收客户机中得到反映。原因在于,因为对象发送过一次,所以 ObjectOutputStream 认为对象已经被传输,并且将只发送 ID,从而否定对象自发送以来发生的任何更改。为了避免这种情况,不要对通过网络发送的对象进行更改。这条规则也适用于对象图中的子对象。

第二个问题是,因为 ObjectOutputStream 维护一个已发送对象及其对象 id 的列表,所以如果发送大量对象,则已发送对象到键的字典会无限增长,导致长时间运行的程序内存不足。为了缓解这个问题,您可以调用 ObjectOutputStream.reset(),这将清除已发送对象的字典。或者,您可以调用 ObjectOutputStream . write unshared()来不缓存 object output stream 字典中的对象。

8-5.获取 Java 执行路径

问题

你想得到 Java 程序运行的路径。

解决办法

调用系统类的 getProperty 方法。例如:

String  path = System.getProperty("user.dir");

它是如何工作的

当 Java 程序启动时,JDK 会更新 user.dir 系统属性,以记录调用 JDK 的位置。该解决方案示例将属性名“user.dir”传递给 getProperty 方法,该方法返回值。

8-6.复制文件

问题

你需要把一个文件从一个文件夹复制到另一个文件夹。

解决办法

在缺省文件系统中,创建文件/文件夹所在的“to”和“From”路径,然后使用 Files.copy 静态方法在创建的路径之间复制文件:

FileSystem fileSystem = FileSystems.getDefault();
Path sourcePath = fileSystem.getPath("file.log");
Path targetPath = fileSystem.getPath("file2.log");
System.out.println("Copy from "+sourcePath.toAbsolutePath().toString()+
" to "+targetPath.toAbsolutePath().toString());
try {
    Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    e.printStackTrace();
}

它是如何工作的

在新的 NIO.2 库中,Java 使用一个抽象层,允许对属于底层操作系统的文件属性进行更直接的操作。

getDefaults()获取可用的抽象系统,您可以在其上进行文件操作。例如,在 Windows 中运行这个示例将得到一个 WindowsFileSystem 如果您在 Linux 中运行这个例子,将会返回一个 LinuxFileSystem 对象;在 OS X 上,返回一个 MacOSXFileSystem。AllFileSystems 支持基本操作;此外,每个具体的文件系统都提供了对该操作系统独有特性的访问。

获得默认的文件系统对象后,您可以查询文件对象。在 NIO.2 文件中,文件夹和链接都称为路径。一旦获得了路径,就可以用它来执行操作。在此示例中,使用源路径和目标路径调用 Files.copy。最后一个参数指的是不同的复制选项。不同的复制选项取决于文件系统,因此请确保您选择的选项与您打算在其中运行应用的操作系统兼容。

8-7.移动文件

问题

您需要将文件从一个文件系统位置移动到另一个位置。

解决办法

如在方法 8-6 中,您使用默认文件系统创建“到”和“从”路径,并调用 Files.move()静态方法:

FileSystem fileSystem = FileSystems.getDefault();
Path sourcePath = fileSystem.getPath("file.log");
Path targetPath = fileSystem.getPath("file2.log");
System.out.println("Copy from "+sourcePath.toAbsolutePath().toString()+
                        " to "+targetPath.toAbsolutePath().toString());
try {
    Files.move(sourcePath, targetPath);
} catch (IOException e) {
    e.printStackTrace();
}

它是如何工作的

以与复制文件相同的方式,创建源和目标的路径。获得源路径和目标路径后,Files.move 会自动将文件从一个位置移动到另一个位置。Files 对象提供的其他方法如下:

  • Delete (path):删除文件(或文件夹,如果它是空的)。

  • Exists (path):检查文件/文件夹是否存在。

  • isDirectory (path):检查创建的路径是否指向一个目录。

  • isExecutable (path):检查文件是否是可执行文件。

  • isHidden (path):检查文件在操作系统中是可见还是隐藏。

8-8.创建目录

问题

您需要从 Java 应用创建一个目录。

解决方案 1

通过使用默认文件系统,您实例化了一个指向新目录的路径;然后调用 Files.createDirectory()静态方法,该方法创建路径中指定的目录。

FileSystem fileSystem = FileSystems.getDefault();
Path directory= fileSystem.getPath("./newDirectory");
try {
    Files.createDirectory(directory);
} catch (IOException e) {
    e.printStackTrace();
}

解决方案 2

如果使用*nix 操作系统,您可以通过调用 PosixFilePermission()方法来指定文件夹属性,该方法允许您在所有者、组和全局级别设置访问权限。例如:

FileSystem fileSystem = FileSystems.getDefault();
Path directory= fileSystem.getPath("./newDirectoryWPermissions");
try {
    Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---");
    FileAttribute<Set<PosixFilePermission>> attr =
         PosixFilePermissions.asFileAttribute(perms);
    Files.createDirectory(directory, attr);

} catch (IOException e) {
    e.printStackTrace();
}

它是如何工作的

Files.createDirectory()方法将路径作为参数,然后创建目录,如解决方案 1 所示。默认情况下,创建的目录将继承默认权限。如果您想在 Linux 中指定特定的权限,可以在 createDirectory()方法中使用 PosixAttributes 作为额外的参数。解决方案 2 演示了传递一组 PosixFilePermissions 来设置新创建的目录的权限的能力。

8-9.遍历目录中的文件

问题

你需要扫描目录中的文件。可能有包含更多文件的子目录。你想把这些包括在你的扫描中。

解决办法

使用 NIO.2 创建 FileVisitor 对象,并在其 visitFile 方法中执行所需的实现。接下来,获取默认的文件系统对象,并通过 getPath()方法获取您想要扫描的路径的引用。最后,调用 Files.walkFileTree()方法,传递路径和您创建的 FileVisitor。下面的代码演示了如何执行这些任务。

FileVisitor<Path> myFileVisitor = new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                        throws IOException {
        System.out.println("Visited File: "+file.toString());
        return FileVisitResult.CONTINUE;
    }
};

FileSystem fileSystem = FileSystems.getDefault();
Path directory= fileSystem.getPath(".");
try {
    Files.walkFileTree(directory, myFileVisitor);
} catch (IOException e) {
    e.printStackTrace();
}

它是如何工作的

在 NIO.2 之前,尝试遍历目录树涉及到递归,并且根据实现的不同,这可能非常脆弱。获取文件夹中文件的调用是同步的,在返回之前需要扫描整个目录;对应用用户生成看似无响应的方法调用。使用 NIO.2,可以指定从哪个文件夹开始遍历,NIO.2 调用将处理递归细节。您提供给 NIO.2 API 的唯一一项是一个类,该类告诉它在找到文件/文件夹时做什么(SimpleFileVisitor 实现)。NIO.2 使用访问者模式,因此不需要预扫描整个文件夹,而是在遍历文件时处理文件。

SimpleFileVisitor 类作为匿名内部类的实现包括重写 visitFile(路径文件,BasicFileAttributesattrs()方法。当您重写此方法时,可以指定遇到文件时要执行的任务。

visitFile 方法返回 FileVisitReturn 枚举。然后,该枚举告诉 FileVisitor 要采取的操作:

  • 继续:继续遍历目录树。

  • 终止:停止遍历。

  • SKIP_SUBTREE:停止从当前树级别继续深入(仅当 preVisitDirectory()方法返回此枚举时有用)。

  • SKIP_SIBLINGS:跳过与当前目录在同一树级别的其他目录。

除了 visitFile()方法之外,SimpleFileVisitor 类还包含以下内容:

  • preVisitDirectory:在进入要遍历的目录之前调用。

  • postVisitDirectory:在遍历完一个目录后调用。

  • visitFile:在访问文件时调用,如示例代码所示。

  • visitFileFailed:如果无法访问文件,则调用该函数;例如,在 I/O 错误时。

8-10.查询(和设置)文件元数据

问题

您需要获得关于特定文件的信息,比如文件大小、它是否是一个目录等等。此外,您可能希望在 Windows 操作系统中将文件标记为存档或在*nix 操作系统中授予特定的 POSIX 文件权限(参考方法 8-8)。

解决办法

使用 Java NIO.2,只需调用 java.nio.file.Files 实用程序类上的方法,传递想要获取元数据的路径,就可以获取任何文件信息。您可以通过调用 Files.getFileAttributeView()方法来获取属性信息,并传递您想要使用的属性视图的特定实现。下面的代码演示了这些获取元数据的技术。

Path path = FileSystems.getDefault().getPath("./file2.log");
try {
    // General file attributes, supported by all Java systems
    System.out.println("File Size:"+Files.size(path));
    System.out.println("Is Directory:"+Files.isDirectory(path));
    System.out.println("Is Regular File:"+Files.isRegularFile(path));
    System.out.println("Is Symbolic Link:"+Files.isSymbolicLink(path));
    System.out.println("Is Hidden:"+Files.isHidden(path));
    System.out.println("Last Modified Time:"+Files.getLastModifiedTime(path));
    System.out.println("Owner:"+Files.getOwner(path));

    // Specific attribute views.
    DosFileAttributeView view = Files.getFileAttributeView(path,
       DosFileAttributeView.class);
    System.out.println("DOS File Attributes\n");
    System.out.println("------------------------------------\n");
    System.out.println("Archive  :"+view.readAttributes().isArchive());
    System.out.println("Hidden   :"+view.readAttributes().isHidden());
    System.out.println("Read-only:"+view.readAttributes().isReadOnly());
    System.out.println("System   :"+view.readAttributes().isSystem());

    view.setArchive(false);

} catch (IOException e) {
    e.printStackTrace();
}

它是如何工作的

与旧的 I/O 技术相比,Java NIO.2 在获取和设置文件属性方面具有更大的灵活性。NIO.2 将不同的操作系统属性抽象为一组“通用”属性和一组“特定于操作系统”的属性。标准属性如下:

  • isDirectory:如果是目录,则为 True。

  • isRegularFile:如果文件不是常规文件、文件不存在或无法确定它是否是常规文件,则返回 false。

  • issymbolick:如果链接是符号性的(在 Unix 系统中最普遍),则为 True。

  • isHidden:如果文件被认为隐藏在操作系统中,则为 True。

  • LastModifiedTime:文件上次更新的时间。

  • Owner:操作系统中文件的所有者。

此外,NIO.2 允许输入底层操作系统的特定属性。为此,首先需要获得一个表示操作系统文件属性的视图(在本例中,它是一个 DosFileAttributeView)。获得视图后,您可以查询和更改特定于操作系统的属性。

注意

AttributeView 仅适用于目标操作系统(不能在*nix 机器中使用 DosFileAttributeView)。

8-11.监视目录的内容变化

问题

您需要跟踪目录内容何时发生了变化(例如,添加、更改或删除了一个文件),并根据这些变化采取行动。

解决办法

通过使用 WatchService,您可以订阅文件夹中发生的事件的通知。在下面的示例中,我们订阅了 ENTRY_CREATE、ENTRY_MODIFY 和 ENTRY_DELETE 事件:

try {
    System.out.println("Watch Event, press q<Enter> to exit");
    FileSystem fileSystem = FileSystems.getDefault();
    WatchService service = fileSystem.newWatchService();
    Path path = fileSystem.getPath(".");
    System.out.println("Watching :"+path.toAbsolutePath());
    path.register(service, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
    boolean shouldContinue = true;
    while(shouldContinue) {
        WatchKey key = service.poll(250, TimeUnit.MILLISECONDS);

        // Code to stop the program
        while (System.in.available() > 0) {
            int readChar = System.in.read();
            if ((readChar == 'q') || (readChar == 'Q')) {
                shouldContinue = false;
                break;
            }
        }
        if (key == null) continue;
        key.pollEvents().stream()
                .filter((event) -> !(event.kind() == StandardWatchEventKinds.OVERFLOW))
                .map((event) -> (WatchEvent<Path>)event).forEach((ev) -> {
            Path filename = ev.context();
            System.out.println("Event detected :"+filename.toString()+" "+ev.kind());
        });
        boolean valid = key.reset();
        if (!valid) {
            break;
        }
    }
} catch (IOException | InterruptedException e) {
    e.printStackTrace();
}

它是如何工作的

NIO.2 包含一个内置的轮询机制来监控文件系统中的变化。使用轮询机制允许您以指定的时间间隔等待事件并轮询更新。一旦事件发生,您就可以处理和消费它。消费事件告诉 NIO.2 框架,您已经准备好处理新事件。

若要开始监视文件夹,请创建一个 WatchService,您可以使用它来轮询更改。创建 WatchService 后,用一个路径注册 WatchService。路径代表文件系统中的一个文件夹。当 WatchService 在 path 中注册时,您定义想要监视的事件种类(参见表 8-2 )。

表 8-2。观察事件的类型
|

WatchEvent

|

描述

|
| --- | --- |
| 泛滥 | 溢出的事件(忽略) |
| 条目 _ 创建 | 创建了一个目录或文件 |
| 条目 _ 删除 | 目录或文件已被删除 |
| 录入 _ 修改 | 目录或文件已被修改 |

在用路径注册了 WatchService 之后,您可以“轮询”WatchService 的事件发生情况。通过调用 watchService.poll()方法,您将等待文件/文件夹事件在该路径上发生。使用 watchService.poll(int timeout,Timeunit timeUnit)将等到达到指定的超时时间后再继续。如果 watchService 接收到一个事件,或者超过了允许的时间,那么它将继续执行。如果没有事件并且超时,watchService.poll(int timeout)返回的 WatchKey 对象将为 null 否则,返回的 WatchKey 对象将包含已发生事件的相关信息。

因为许多事件可以同时发生(例如,移动整个文件夹或将一堆文件粘贴到一个文件夹中),所以 WatchKey 可能包含多个事件。通过调用 watchKey.pollEvents()方法,可以使用 WatchKey 获取与该键关联的所有事件。

watchKey.pollEvents()调用将返回可以迭代的 watchEvents 列表。每个 watchEvent 都包含有关该事件引用的实际文件或文件夹的信息(例如,整个子文件夹可能已被移动或删除),以及事件类型(添加、编辑、删除)。只有那些在 WatchService 上注册的事件才会被处理。您可以注册的事件类型在表 8-2 中列出。

一旦处理了一个事件,调用 EventKey.reset()是很重要的。重置将返回一个布尔值,确定 WatchKey 是否仍然有效。如果 WatchKey 被取消或其原始 WatchService 被关闭,它将变得无效。如果 eventKey 返回 false,您应该中断监视循环。

8-12.读取属性文件

问题

您希望为应用建立一些配置设置,并且希望能够手动或以编程方式修改这些设置。此外,您希望能够动态地更改一些配置,而无需重新编译和重新部署。

解决办法

创建一个属性文件来存储应用配置。使用 Properties 对象,为应用处理加载存储在属性文件中的属性。还可以在属性文件中更新和修改属性。下面的示例演示如何读取名为 properties.conf 的属性文件,加载值以供应用使用,最后设置属性并将其写入文件。

File file = new File("properties.conf");
Properties properties = null;
try {
    if (!file.exists()) {
        file.createNewFile();
    }
    properties = new Properties();

    properties.load(new FileInputStream("properties.conf"));
} catch (IOException e) {
    e.printStackTrace();
}
boolean shouldWakeUp = false;
int startCounter = 100;
String shouldWakeUpProperty = properties.getProperty("ShouldWakeup");
shouldWakeUp = (shouldWakeUpProperty == null) ? false : Boolean.parseBoolean(shouldWakeUpProperty.trim());

String startCounterProperty = properties.getProperty("StartCounter");
try {
    startCounter = Integer.parseInt(startCounterProperty);
} catch (Exception e) {
    System.out.println("Couldn't read startCounter, defaulting to " + startCounter);
}
String dateFormatStringProperty = properties.getProperty("DateFormatString", "MMM dd yy");

System.out.println("Should Wake up? " + shouldWakeUp);
System.out.println("Start Counter: " + startCounter);
System.out.println("Date Format String:" + dateFormatStringProperty);

//setting property
properties.setProperty("StartCounter", "250");
try {
properties.store(new FileOutputStream("properties.conf"), "Properties Description");
} catch (IOException e) {
    e.printStackTrace();
}
properties.list(System.out);

它是如何工作的

Java Properties 类帮助您管理程序属性。它允许您通过外部修改(有人编辑属性文件)或使用 Properties.store()方法在内部管理属性。

Properties 对象既可以在没有文件的情况下实例化,也可以在有预加载文件的情况下实例化。Properties 对象读取的文件采用[name]=[value]的形式,并以文本形式表示。如果需要以其他格式存储值,则需要写入和读取字符串。

如果您希望在程序之外修改文件(用户直接打开文本编辑器并更改值),请确保对输入进行净化;比如修剪值以留出额外的空格,如果需要的话忽略大小写。

若要以编程方式查询不同的属性,请调用 getProperty(String)方法,传递要检索其值的属性的基于字符串的名称。如果找不到该属性,该方法将返回 null。或者,您可以调用 getProperty (String,String)方法,如果在 Properties 对象中没有找到该属性,它将返回第二个参数作为其值。如果文件中没有特定键的条目,那么指定默认值是一个很好的做法。

在查看生成的属性文件时,您会注意到前两行指出了文件的描述和修改日期。这两行以#开头,这在 Java 属性文件中相当于注释。处理文件时,Properties 对象将跳过任何以#开头的行。

注意

如果您允许用户直接修改您的配置文件,那么在从 properties 对象中检索属性时进行验证是非常重要的。属性值中最常见的问题之一是前导和/或尾随空格。如果指定布尔值或整数属性,请确保可以从字符串中解析它们。至少,在试图解析一个非常规值时捕获一个异常(并记录违规值)。

8-13.解压缩文件

问题

您的应用需要从压缩的。压缩文件。

解决办法

使用 Java.util.zip 包,您可以打开一个. zip 文件并遍历它的条目。在遍历条目时,可以为目录条目创建目录。类似地,当遇到文件条目时,将解压缩后的文件写入文件。下面几行代码演示了如何执行解压缩和文件迭代技术,如上所述。

ZipFile file = null;
try {
    file = new ZipFile("file.zip");
    FileSystem fileSystem = FileSystems.getDefault();
    Enumeration<? extends ZipEntry> entries = file.entries();
    String uncompressedDirectory = "uncompressed/";
    Files.createDirectory(fileSystem.getPath(uncompressedDirectory));
    while (entries.hasMoreElements()) {
        ZipEntry entry = entries.nextElement();
        if (entry.isDirectory()) {
            System.out.println("Creating Directory:" + uncompressedDirectory + entry.getName());
            Files.createDirectories(fileSystem.getPath(uncompressedDirectory +
                                    entry.getName()));
        } else {
            InputStream is = file.getInputStream(entry);
            System.out.println("File :" + entry.getName());
            BufferedInputStream bis = new BufferedInputStream(is);

            String uncompressedFileName = uncompressedDirectory + entry.getName();
            Path uncompressedFilePath = fileSystem.getPath(uncompressedFileName);
            Files.createFile(uncompressedFilePath);
            try (FileOutputStream fileOutput = new FileOutputStream(uncompressedFileName)) {
                while (bis.available() > 0) {
                    fileOutput.write(bis.read());
                }
            }
            System.out.println("Written :" + entry.getName());
        }
    }
} catch (IOException e) {
    e.printStackTrace();
}

它是如何工作的

要处理. Zip 存档文件的内容,请创建一个 ZipFile 对象。可以实例化一个 ZipFile 对象,将. zip 档案的名称传递给构造函数。创建对象后,您可以访问指定的. zip 文件信息。每个 ZipFile 对象将包含一个条目集合,这些条目代表归档中包含的目录和文件,通过遍历这些条目,您可以获得每个压缩文件的信息。每个 ZipEntry 实例都有压缩和未压缩的大小、名称和未压缩字节的输入流。

未压缩的字节可以通过生成 InputStream 读入字节缓冲区,然后(在我们的例子中)写入文件。使用 FileStream,可以确定在不阻塞进程的情况下可以读取多少字节。一旦读取了确定数量的字节,就将这些字节写入输出文件。这个过程一直持续到读取完总字节数为止。

注意

如果文件非常大,将整个文件读入内存可能不是一个好主意。如果您需要处理一个大文件,最好先将它以未压缩的格式写入磁盘(如示例所示),然后打开它并分块加载。如果您正在处理的文件不是很大(您可以通过检查 getSize()方法来限制大小),您可以将它加载到内存中。

8-14.管理操作系统进程

问题

您希望能够从 Java 应用中识别和控制本机操作系统进程。

解决办法

利用 Java 9 中增强的进程 API 来获取有关单个操作系统进程的信息或销毁它们。在本例中,我们将调用 ProcessHandle.info()方法来检索有关操作系统进程的信息。特别是,我们将查看当前正在运行的 JVM 进程,并从中启动另一个进程。最后,我们将询问新流程。

import java.lang.ProcessBuilder;
import java.lang.Process;
import java.time.Instant;
import java.time.Duration;
import java.time.temporal.ChronoUnit;

public class Recipe08_14 {

  public static void printProcessDetails(ProcessHandle currentProcess){
    //Get the instance of process info
    ProcessHandle.Info currentProcessInfo = currentProcess.info();
    if ( currentProcessInfo.command().orElse("").equals("")){
      return;
    }
    //Get the process id
    System.out.println("Process id: " + currentProcess.getPid());
    //Get the command pathname of the process
    System.out.println("Command: " + currentProcessInfo.command().orElse(""));
    //Get the arguments of the process
    String[] arguments = currentProcessInfo.arguments().orElse(new String[]{});
    if ( arguments.length != 0){
      System.out.print("Arguments: ");
      for(String arg : arguments){
        System.out.print(arg + " ");
      }
      System.out.println();
    }
    //Get the start time of the process
    System.out.println("Started at: " + currentProcessInfo.startInstant().orElse(Instant.now()).toString());
    //Get the time the process ran for
    System.out.println("Ran for: " + currentProcessInfo.totalCpuDuration().orElse(Duration.ofMillis(0)).toMillis() + "ms");
    //Get the owner of the process
    System.out.println("Owner: " + currentProcessInfo.user().orElse(""));
  }

  public static void main(String[] args){
    ProcessHandle current = ProcessHandle.current();
    ProcessHandle.Info currentInfo = current.info();
    System.out.println("Command Line Process: " + currentInfo.commandLine());
    System.out.println("Process User: " + currentInfo.user());
    System.out.println("Process Start Time: " + currentInfo.startInstant());
    System.out.println("PID: " + current.getPid());

    ProcessBuilder pb = new ProcessBuilder("ls");
    try {
    Process process = pb.start();
    System.out.println(process);
    process.children()
    .forEach((p) ->{
     System.out.println(p);
   });
      ProcessHandle pHandle = process.toHandle();
     System.out.println("Parent of Process: " + pHandle.parent());
    } catch (java.io.IOException e){
      System.out.println(e);
    }

  }
}

结果:

Command Line Process: Optional[/Library/Java/JavaVirtualMachines/jdk1.9.0.jdk/Contents/Home/bin/java Recipe0814]
Process User: Optional[Juneau]
Process Start Time: Optional[2016-02-20T06:14:56.064Z]
PID: 10892
java.lang.ProcessImpl@7c30a502
Parent of Process: Optional.empty

它是如何工作的

Java 9 中的 process API 得到了增强,提供了获取有关操作系统进程的有价值信息的能力。API 中添加了 ProcessHandle 接口,提供了一个 info()方法,可用于查询指定的进程和检索更多信息。添加了许多其他有用的实用程序方法来获取有关指定进程的信息。

ProcessHandle。Info 对象是当前进程的信息快照,通过调用 ProcessHandle info()方法返回。ProcessHandle。Info 可以用来返回进程的可执行命令、进程开始时间以及其他一些有用的特性。表 8-3 显示了处理 Handle.Info 的不同方法。

页:1。ProcessHandle(处理程序处理程序)。关于
|

方法

|

描述

|
| --- | --- |
| 参数() | 返回流程参数的字符串数组。 |
| 命令 | 返回进程的可执行路径名。 |
| 命令行() | 返回进程的命令行。 |
| startInstant() | 返回进程的开始时间。 |
| 总计持续时间() | 返回进程累计的总 CPU 时间。 |
| 用户() | 返回运行进程的用户。 |

ProcessHandle 接口可用于返回信息,如进程子进程、PID(进程 ID)、父进程等等。它还可以用来确定一些有用的信息,比如进程是否还活着。表 8-4 显示了 ProcessHandle 的不同方法。

页:1。ProcessHandle(处理程序处理程序)
|

方法

|

返回

|

描述

|
| --- | --- | --- |
| 所有进程() | 静态流 | 返回当前进程可见的所有进程的快照。 |
| 儿童() | 流 | 返回当前进程的直接子进程的快照。 |
| 比较() | (同 Internationalorganizations)国际组织 | 将一个 ProcessHandle 与另一个 process handle 进行比较,并返回顺序。 |
| 当前() | 静态进程句柄 | 返回当前进程的 ProcessHandle。 |
| 后代 _) | 流 | 返回当前进程后代的快照。 |
| 销毁() | 布尔 | 请求终止当前进程。 |
| destroyForcibly() | 布尔 | 请求强制终止当前进程。 |
| 等于(对象) | 布尔 | 将当前进程与另一个对象进行比较,如果该对象不为空,则返回 true,并表示同一个系统进程。 |
| getPid() | 长的 | 返回当前进程的本机进程 ID。 |
| hashCode() | (同 Internationalorganizations)国际组织 | 返回当前 ProcessHandle 的哈希代码值。 |
| 信息() | ProcessHandle.info | 返回当前进程的信息快照。 |
| isalive() | 布尔 | 测试当前进程是否处于活动状态。 |
| (长管道仪表流程图) | 静态可选 | 为现有流程返回可选的 |
| onExit() | 可完成的未来 | 返回当前进程的 CompleteableFuture |
| 父级() | 可选 | 为当前进程的父进程返回可选的 |
| 支持 NormalTermination() | 布尔 | 如果当前进程的实现包含支持正常进程终止的 destroy()方法,则返回 true。 |

要利用该 API,请调用 ProcessHandle.info()方法来检索 ProcessHandle.info 对象。然后,该对象可用于执行命令,或检索有关进程的信息。如果与 Process 和 ProcessBuilder 类一起使用,API 可用于生成、监控和终止操作系统进程。

摘要

本章演示了几个在 Java 中使用文件和网络 I/O 的例子。您了解了如何序列化文件以便将它们存储到磁盘上,还了解了如何使用 Java APIs 操作主机的文件系统。本章还讲述了如何读写属性文件,以及如何执行文件压缩。最后,本章介绍了 Java 9 中添加的流程 API 的新特性。

九、异常和日志记录

异常是描述程序中异常情况的一种方式。它们是发生了意想不到的事情的指示器。因此,异常可以有效地中断程序的当前流程,并发出需要注意的信号。因此,明智地利用异常的程序受益于更好的控制流,并且对用户来说变得更加健壮和有用。即便如此,不加选择地使用异常也会导致性能下降。

在 Java 中,异常可以由抛出捕获。抛出异常包括向代码表明遇到了异常,使用 throw 关键字通知 JVM 在当前栈中找到任何能够处理这种异常情况的代码。捕捉异常包括告诉编译器可以处理哪些异常,以及应该监视代码的哪个部分以防止这些异常发生。这在 try/catch Java 块中表示(在方法 9-1 中描述)

所有异常都继承自 Throwable,如图 9-1 所示。从 Throwable 继承的类可以在 try/catch 语句的 catch 子句中定义。JVM 主要使用错误类来表示严重和/或致命的错误。根据 Java 文档,应用不被期望捕获错误异常,因为它们被认为是致命的(想象一下计算机着火了)。Java 程序中的大部分异常都是从 Exception 类继承的。

A323910_3_En_9_Fig1_HTML.jpg

图 9-1。Java 中异常类层次结构的一部分

在 JVM 中有两种类型的异常:检查的和未检查的。检查的异常由方法强制执行。在方法签名中,您可以指定方法可以抛出的异常的种类。这要求该方法的任何调用方创建一个 try/ catch 块,该块处理方法签名中声明的异常。未检查的异常不需要如此严格的约定,并且可以在任何地方自由抛出,而无需强制实现 try/catch 块。尽管如此,未检查的异常(如配方 9-6 所述)通常是不鼓励的,因为它们会导致线程散开(如果没有任何东西捕捉到异常)和问题的不可见性。从 RuntimeExceptionare 继承的异常类被视为未检查异常,而直接从 Exception 继承的异常类被视为检查异常。

请注意,抛出异常的代价很高(与其他语言构造替代方法相比),因此抛出异常并不能很好地替代控制流。例如,您不应该抛出异常来指示方法调用的预期结果(比如像 isUsernameValid(字符串用户名)这样的方法)。更好的做法是调用方法并返回一个布尔值,而不是试图引发一个 InvalidUsernameException 来指示失败。

虽然异常在可靠的软件开发中起着重要的作用,但是异常的日志记录也同样重要。应用中的日志记录有助于开发人员理解正在发生的事件,而无需调试代码。在没有机会进行实时调试的生产环境中尤其如此。从这个意义上说,日志记录收集了正在发生的事情的线索(很可能是哪里出错了),并帮助您解决生产问题。许多开发人员选择利用结构化日志记录框架来为应用提供更健壮的日志记录。一个可靠的日志框架和一个合理的方法将会省去许多深夜工作时的疑惑,“发生了什么?”

Java 的日志已经非常成熟了。有许多开源项目被广泛接受为日志记录的事实上的标准。在本章的菜谱中,您将使用 Java 的日志框架和 Java 的简单日志外观(SLF4J)。这两个项目一起为大多数日志记录需求创建了一个足够好的解决方案。对于涉及 SLF4J 和 Log4j 的菜谱,下载 http://www.slf4j.org/()并将其放入项目的依赖路径中。本章还将涉及 Java 9 版本中添加的低级 JVM 日志。

9-1.捕捉异常

问题

您希望优雅地处理代码中生成的任何异常。

解决办法

使用内置的 try/catch 语言构造来捕捉异常。通过将任何可能引发异常的代码块包装在 try/catch 块中来实现这一点。在下面的示例中,使用了一个方法来生成一个布尔值,以指示指定字符串的长度是否大于五个字符。如果作为参数传递的字符串为 null,则 length()方法会抛出一个 NullPointerException,并在 catch 块中被捕获。

private void start() {
    System.out.println("Is th String 1234 longer than 5 chars?:"+
            isStringShorterThanFiveCharacters("1234"));
    System.out.println("Is th String 12345 longer than 5 chars?:"+
            isStringShorterThanFiveCharacters("12345"));
    System.out.println("Is th String 123456 longer than 5 chars?:"+
            isStringShorterThanFiveCharacters("123456"));
    System.out.println("Is th String null longer than 5 chars?:"+
            isStringShorterThanFiveCharacters(null));

}

private boolean isStringShorterThanFiveCharacters(String aString) {
    try {
        return aString.length() > 5;
    } catch (NullPointerException e) {
        System.out.println("An Exception Occurred: " + e);
        return false;
    }
}

它是如何工作的

try 关键字指定包含的代码段有可能引发异常。catch 子句放在 try 子句的末尾。每个 catch 子句指定正在捕获哪个异常。如果没有为检查的异常提供 catch 子句,编译器将生成错误。两种可能的解决方案是添加一个 catch 子句,或者在封闭方法的 throws 子句中包含异常。任何被抛出但未被捕获的检查异常都将在调用栈中向上传播。如果这个方法没有捕捉到异常,执行代码的线程就会终止。如果终止线程是程序中唯一的线程,它终止程序的执行。

如果 try 子句需要捕获多个异常,可以指定多个异常,用竖线字符分隔。例如,下面的 try/catch 块可用于捕获 NumberFormatException 和 NullPointerException。

try {
  // code here
} catch (NumberFormatException|NullPointerException ex) {
  // logging

}

有关捕捉多个异常的更多信息,请参见配方 9-4。

注意

抛出异常时要小心。如果抛出的异常没有被捕获,它将在调用栈中向上传播;如果没有任何 catch 子句能够处理该异常,它将导致正在运行的线程终止(也称为解开)。如果你的程序只有一个主线程,一个未被捕获的异常将终止你的程序。

9-2.保证代码块被执行

问题

您希望编写当控件离开代码段时执行的代码,即使控件由于引发错误或代码段异常结束而离开。例如,您获得了一个锁,并希望确保正确地释放它。您希望在出现错误时释放锁,也希望在没有错误时释放锁。

解决办法

使用 try/catch/finally 块来正确释放您在代码段中获取的锁和其他资源。将您希望在不考虑异常的情况下执行的代码放入 finally 子句中。在该示例中,finally 关键字指定了一个将始终执行的代码块,而不管 try 块中是否引发了异常。在 finally 块中,通过调用 lock.unlock()来释放锁:

private void callFunctionThatHoldsLock() {
    myLock.lock();
    try {
        int number = random.nextInt(5);
        int result = 100 / number;
        System.out.println("A result is " + result);
        FileOutputStream file = new FileOutputStream("file.out");
        file.write(result);
        file.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        myLock.unlock();
    }
}

它是如何工作的

放置在 try/catch/finally 块的 finally 子句中的代码将始终被执行。在本例中,通过在函数开始时获取锁,然后在 finally 块中释放锁,可以保证无论是否抛出异常(检查或未检查),锁都将在函数结束时释放。总之,获得的锁应该总是在 finally 块中释放。在示例中,假设 mylock.unlock()函数调用不在 finally 块中(而是在 try 块的末尾);如果在这种情况下发生异常,对 mylock.unlock()的调用将不会发生,因为代码执行将在异常发生的位置中断。在这种情况下,锁将永远被获取,永远不会被释放。

警告

如果需要在方法上返回值,那么在 finally 块中返回值时要非常小心。finally 块中的 return 语句将始终执行,而不管 try 块中可能发生的任何其他 return 语句。

9-3.抛出异常

问题

如果应用中出现某种情况,您希望通过引发异常来中止当前代码路径的执行。

解决办法

使用 throw 关键字在发生这种情况时引发指定的异常。使用 throw 关键字,可以通知当前线程寻找 try/catch 块(在当前级别和栈上),它可以处理抛出的异常。在下面的示例中,如果传入的参数为 null,callSomeMethodThatMightThrow 将引发 NullPointerException。

private void start() {
    try {
        callSomeMethodThatMightThrow(null);
    } catch (IllegalArgumentException e) {
        System.out.println("There was an illegal argument exception!");
    }

}

private void callSomeFunctionThatMightThrow(Object o) {
    if (o == null) throw new NullPointerException("The object is null");

}

在此代码示例中,方法 callSomeMethodThatMightThrow 检查以确保向其传递了有效的参数。如果参数为 null,那么它会抛出一个 NullPointerException,表明这个方法的调用者用错误的参数调用了它。

它是如何工作的

throw 关键字允许您显式生成异常条件。当当前线程抛出异常时,它不会执行 throw 语句之外的任何内容,而是将控制转移到 catch 子句(如果有)或终止线程。

注意

当抛出异常时,请确保您打算这样做。如果异常在栈中向上传播时没有被捕获,它将终止正在执行的线程(也称为解开)。如果你的程序只有一个主线程,一个未被捕获的异常将终止你的程序。

9-4.捕捉多个异常

问题

应用中的代码块有可能引发多个异常。您希望捕获 try 块中可能出现的每个异常。

解决方案 1

在同一个块中可能遇到多个异常的情况下,可以指定多个 catch 子句。每个 catch 子句可以指定一个不同的异常来处理,这样每个异常都可以用不同的方式来处理。在下面的代码中,使用了两个 catch 子句来处理 IOException 和 ClassNotFoundException。

try {
    Class<?> stringClass = Class.forName("java.lang.String");
    FileInputStream in = new FileInputStream("myFile.log") ; // Can throw IOException
    in.read();

} catch (IOException e) {
    System.out.println("There was an exception "+e);
} catch (ClassNotFoundException e) {
    System.out.println("There was an exception "+e);
}

解决方案 2

如果您的应用倾向于在单个块中抛出多个异常,那么可以使用竖线操作符(|)以相同的方式处理每个异常。在下面的示例中,catch 子句指定了用竖线(|)分隔的多个异常类型,以相同的方式处理每个异常。

        try {
            Class<?> stringClass = Class.forName("java.lang.String");
            FileInputStream in = new FileInputStream("myFile.log") ;
// Can throw IOException
            in.read();

        } catch (IOException | ClassNotFoundException e) {
            System.out.println("An exception of type "+e.getClass()+" was thrown! "+e);
        }

它是如何工作的

有几种不同的方法来处理可能引发多个异常的情况。您可以指定单独的 catch 子句,以不同的方式处理每个异常。要以相同的方式处理每个异常,您可以使用一个 catch 子句,并用竖线操作符分隔每个异常。

注意

如果您在多个 catch 块中捕获一个异常(解决方案 1),请确保 catch 块是从最具体的到最一般的定义的。不遵循这一约定将会阻止更具体的块处理异常。当有 catch (Exception e)块时,这是最重要的,它可以捕获几乎所有的异常。

拥有一个 catch (Exception e)块——称为 catch-all 或 Pokémon 异常处理程序(必须捕获所有异常)——通常是一种糟糕的做法,因为这样的块将捕获每种异常类型,并对它们一视同仁。这就成了一个问题,因为该块可以捕获其他异常,这些异常可能发生在调用栈的更深处,而您可能不希望该块捕获这些异常(OutOfMemoryException)。最佳实践是指定每个可能的异常,而不是指定一个捕获所有异常的异常处理程序来捕获所有异常。

9-5.捕获未捕获的异常

问题

您想知道线程何时因未捕获的异常(如 NullPointerException)而终止。

解决方案 1

当创建一个 Java 线程时,有时您需要确保任何异常都被捕获并得到正确处理,以帮助确定线程终止的原因。为此,Java 允许您为每个线程或全局注册一个 ExceptionHandler()。下面的代码演示了一个基于每个线程注册异常处理程序的示例。

private void start() {
    Thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e) -> {
        System.out.println("Woa! there was an exception thrown somewhere! "+t.getName()+": "+e);
    });

    final Random random = new Random();
    for (int j = 0; j < 10; j++) {
        int divisor = random.nextInt(4);
        System.out.println("200 / " + divisor + " Is " + (200 / divisor));
    }
}

该线程中的 for 循环将正确执行,直到遇到异常,此时将调用 DefaultUncaughtExceptionHandler。UncaughtExceptionHandler 是一个函数接口,因此可以利用 lambda 表达式来实现异常处理程序。

解决方案 2

可以在特定线程上注册一个 UncaughtExceptionHandler。这样做之后,线程内发生的任何未被捕获的异常都将由 UncaughtExceptionHandler()的 uncaughtException()方法处理。例如:

private void startForCurrentThread() {
    Thread.currentThread().setUncaughtExceptionHandler((Thread t, Throwable e) -> {
        System.out.println("In this thread "+t.getName()+" an exception was thrown "+e);
    });

    Thread someThread = new Thread(() -> {
        System.out.println(200/0);
    });
    someThread.setName("Some Unlucky Thread");
    someThread.start();

    System.out.println("In the main thread "+ (200/0));
}

在前面的代码中,在 currentThread 上注册了一个 UncaughtExceptionHandler。就像解决方案 1 一样,UncaughtExceptionHandler 是一个函数接口,因此可以利用 lambda 表达式来实现异常处理程序。

它是如何工作的

对于每个未被捕获的未检查异常,将调用 thread . defaultuncaughtexceptionhandler()。当 UncaughtExceptionHandler()处理异常时,这意味着没有适当的 try/catch 块来捕获异常。因此,异常在线程栈中一路冒泡。这是该线程终止前执行的最后一段代码。当在线程的或默认的 UncaughtExceptionHandler()上捕捉到异常时,线程将终止。UncaughtExceptionHandler()可用于记录关于异常的信息,以帮助查明异常的原因。

在第二个解决方案中,专门为当前线程设置了 UncaughtExceptionHandler()。当线程抛出一个未被捕获的异常时,它将冒泡到线程的 UncaughtExceptionHandler()。如果不存在,它将冒泡到 defaultUncaughtExceptionHandler()。同样,在这两种情况下,引发异常的线程都将终止。

小费

在处理多线程时,显式命名线程总是一个好的做法。这使得确切地知道哪个线程导致了异常变得更加容易,而不是必须跟踪一个像 Thread-##(未命名线程的默认命名模式)这样命名的未知线程。

9-6.使用 try/catch 块管理资源

问题

如果出现异常,您需要确保 try/catch 块中使用的所有资源都被释放。

解决办法

利用自动资源管理(ARM)特性,该特性可以用 try-with-resources 语句来指定。使用 try-with-resources 语句时,try 子句中指定的任何资源都会在块终止时自动释放。在下面的代码中,FileOutputStream、BufferedOutputStream 和 DataOutputStream 资源由 try-with-resources 块自动处理。

try (
**FileOutputStream fos = new FileOutputStream("out.log");** 
**BufferedOutputStream bos = new BufferedOutputStream(fos);** 
**DataOutputStream dos = new DataOutputStream(bos)** 
) {
    dos.writeUTF("This is being written");
} catch (Exception e) {
    System.out.println("Some bad exception happened ");
}

它是如何工作的

在大多数情况下,您希望在块执行完成后,干净地关闭/释放在 try/catch 块中获取的资源。如果一个程序不关闭/释放它的资源或者不正确地这样做,资源可能会被无限期地获取,导致诸如内存泄漏之类的问题发生。大多数资源都是有限的(文件句柄或数据库连接),因此会导致性能下降(并引发更多异常)。为了避免这些情况,Java 提供了一种在 try/catch 块中发生异常时自动释放资源的方法。通过声明一个 try-with-resources 块,如果在该块中引发异常,则检查 try 块的资源将被关闭。大多数内置到 Java 中的资源将在 try-with-resources 语句中正常工作(有关完整列表,请参见 java.lang.AutoCloseable 接口的实现者)。此外,第三方实现者可以通过实现 AutoCloseable 接口来创建使用 try-with-resources 语句的资源。

try-with-resources 语句的语法包括 try 关键字,后跟一个左括号,然后是您希望在发生异常或块完成时释放的所有资源声明,最后以右括号结束。请注意,如果您试图声明一个没有实现 AutoCloseable 接口的资源/变量,您将会收到一个编译器错误。在右括号之后,try/catch 块的语法与普通块相同。

try-with-resources 特性的主要优点是它允许更干净地释放资源。通常当获取一个资源时,会有很多相互依赖(创建文件处理程序,它们被包装在输出流中,输出流被包装在缓冲流中)。在异常情况下正确关闭和释放这些资源需要检查每个依赖资源的状态并小心地释放它,而这样做需要编写大量代码。相比之下,try-with-resources 构造允许 JVM 处理好资源的适当处置,即使是在异常情况下。

注意

try-with-resources 块将始终关闭已定义的资源,即使没有引发异常。

9-7.创建异常类

问题

您希望创建一种新类型的异常,用于指示特定事件。

解决方案 1

创建一个扩展 java.lang. RuntimeException 的类,创建一个可以随时抛出的异常类。在下面的代码中,由 IllegalChatServerException 标识的类扩展了 RuntimeException,并接受一个字符串作为构造函数的参数。然后,当代码中发生指定的事件时,将引发异常。

class IllegalChatServerException extends RuntimeException {
    IllegalChatServerException(String message) {
        super(message);
    }
}

private void disconnectChatServer(Object chatServer) {
    if (chatServer == null) throw new IllegalChatServerException("Chat server is empty");
}

解决方案 2

创建一个扩展 java.lang.Exception 的类来生成一个检查异常类。需要在栈中捕获或重新抛出检查过的异常。在以下示例中,标识为 ConnectionUnavailableException 的类扩展了 java.lang.Exception,并接受一个字符串作为构造函数的参数。然后,代码中的方法会引发检查到的异常。

class ConnectionUnavailableException extends Exception {
    ConnectionUnavailableException(String message) {
        super(message);
    }
}

private void sendChat(String chatMessage) throws ConnectionUnavailableException {
    if (chatServer == null)
            throw new ConnectionUnavailableException("Can't find the chat server");
}

它是如何工作的

有时候需要创建一个定制的异常,尤其是在创建 API 的时候。通常的建议是使用 JDK 提供的可用异常类之一。例如,使用 IOException 处理与 I/O 相关的问题,或者使用 IllegalArgumentException 处理非法参数。如果没有完全合适的 JDK 异常,您总是可以扩展 java.lang.Exception 或 java.lang.RuntimeException 并实现自己的异常系列。

根据基类的不同,创建异常类相当简单。扩展 RuntimeException 使您能够在任何时候抛出结果异常,而无需将其捕获到栈中。这是有利的,因为 RuntimeException 是一个更宽松的契约,但是如果没有捕获到异常,抛出这样的异常会导致线程终止。相反,扩展 Exception 允许您明确地强制任何引发异常的代码能够在 catch 子句中处理它。然后,契约强制检查的异常实现 catch 处理程序,这可能会避免线程终止。

实际上,我们不鼓励扩展 RuntimeException,因为它会导致糟糕的异常处理。我们的经验是,如果有可能从异常中恢复,您应该通过扩展 exception 来创建相关的异常类。如果不能合理地期望开发人员从异常中恢复(比如 NullPointerException),那么扩展 RuntimeException。

9-8.重新引发捕获的异常

问题

您的应用包含一个多批处理异常,并且您想要重新引发一个以前被捕获的异常。

解决办法

从 catch 块中抛出异常,它将在被捕获的同一类型上再次抛出该异常。在下面的示例中,在代码块中捕获异常,并将其重新引发给方法的调用方。

private void doSomeWork() throws **IOException, InterruptedException** {
    LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();

        try {
            FileOutputStream fos = new FileOutputStream("out.log");
            DataOutputStream dos = new DataOutputStream(fos);
            while (!queue.isEmpty()) {
                dos.writeUTF(queue.take());
            }
        } catch (InterruptedException | IOException e ) {
          e.printStackTrace();
**throw e;** 
        }

    }

它是如何工作的

可以简单地抛出之前捕获的异常,JVM 会将异常冒泡到适当的类型。就像抛出检查异常的情况一样;它也必须在方法声明中定义。在这个解决方案的示例中,doSomeWork()方法抛出一个 IOException 和一个 InterruptedException,这导致调用代码执行一个 try-catch 来适当地处理抛出的异常。

9-9.在应用中记录事件

问题

您希望记录事件、调试消息、错误情况和应用中的其他事件。

解决办法

利用应用中的 SLF4J 和 Java 日志 API 来实现日志解决方案。以下示例首先创建一个名为 recipeLogger 的 logger 对象。在此示例中,SLF4J API 用于记录信息性消息、警告消息和错误消息:

private void loadLoggingConfiguration() {
    FileInputStream ins = null;
    try {
        ins = new FileInputStream(new File("logging.properties"));
        LogManager.getLogManager().readConfiguration(ins);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
private void start() {
    loadLoggingConfiguration();
    Logger logger = LoggerFactory.getLogger("recipeLogger");
    logger.info("Logging for the first Time!");
    logger.warn("A warning to be had");
    logger.error("This is an error!");
}

它是如何工作的

在示例中,loadLogConfiguration()函数打开一个指向 logging.properties 文件的流,并将其传递给 java.util.logging.LogManager()。这样做可以将 java.util.logging 框架配置为使用 logging.properties 文件中指定的设置。然后,在解决方案的 start 方法中,代码获取一个名为 recipeLogger 的 logger 对象。该示例继续通过 recipeLogger 将消息记录到。关于实际测井参数的更多信息可在配方 9-10 中找到。

SLF4J 提供了一个通用的 API,它使用一个简单的外观模式来抽象底层的日志实现。SLF4J 可以用于大多数常见的日志记录框架,如 Java 日志记录 API (java.util.logging)、Log4j、Jakarta Commons Logging 等。在实践中,SLF4J 提供了选择(和交换)日志框架的灵活性,并允许使用 SLF4J 的项目快速集成到应用选择的日志框架中。

要在应用中使用 SLF4J,下载位于 http://www.slf4j.org/的 SLF4J 二进制文件。下载完成后,提取内容并将 slf4j-api-x.x.x.jar 添加到项目中。这是主要的。包含 SLF4J API 的 jar 文件(程序可以调用它来记录信息)。将 slf4j-api-x.x.x.jar 文件添加到项目中后,找到 slf4j-jdk14-x.x.x.jar 并将其添加到项目中。第二个文件表明 SLF4J 将使用 java.util.logging 类来记录信息。

SLF4J 的工作方式是,在运行时 SLF4J 扫描类路径并选择第一个。实现 SLF4J API 的 jar。在示例中,找到并加载了 slf4j-jdk14-x.x.x.jar。这个。jar 代表本地 Java 日志框架(称为 jdk.1.4 日志)。例如,如果您想使用另一个日志记录框架,那么可以将 slf4j-jdk14-x.x.x.jar 替换为所需日志程序的相应 slf4j 实现。例如,要使用 Apache 的 Log4j 日志框架,包括 slf4j-log4j12-x.x.x.jar。

注意

java.util.logging 框架由属性日志文件配置。

一旦配置了 SLF4J,就可以通过调用 SLF4J 日志记录方法在应用中记录信息。这些方法根据日志记录级别记录信息。然后,可以使用日志记录级别来过滤实际记录的消息。按日志级别过滤消息的功能非常有用,因为可能会记录大量信息或调试信息。如果需要对应用进行故障排除,可以更改日志记录级别,并且可以在日志中显示更多信息,而无需更改任何代码。通过级别过滤消息的能力被称为设置日志级别。每个日志框架引用都包含自己的配置文件,用于设置日志级别(除了其他内容之外,如日志文件名和日志文件配置)。在本例中,因为 SLF4J 使用 java.util.logging 框架进行日志记录,所以您需要为所需的日志记录配置 java.util.logging 属性。见表 9-1 。

表 9-1。日志记录级别
|

记录级别

|

建议

|
| --- | --- |
| 微量 | 最不重要的日志事件 |
| 调试 | 用于帮助调试的额外信息 |
| 信息 | 用于日常记录消息 |
| 警告 | 用于可恢复的问题,或怀疑发生错误设置/不标准行为的情况 |
| 错误 | 用于异常、实际错误和您确实需要知道的事情 |
| 致命的 | 最重要 |

注意

设置日志级别时,记录器将在该级别及以下级别进行记录。因此,如果日志记录配置将日志级别设置为 info,则将记录 Info、Warn、Error 和 Fatal 级别的消息。

9-10.旋转和清除日志

问题

您已经开始记录信息,但是记录的信息继续增长,失去控制。您希望在日志文件中只保留最后 250KB 的日志条目。

解决办法

使用 SLF4J 和 java.util.logging 来配置滚动日志。在本例中,名为 recipeLogger 的日志记录器用于记录许多消息。输出将生成滚动日志文件,其中包含重要的 Log0.log 文件中最新记录的信息。

        loadLoggingConfiguration();

        Logger logger = LoggerFactory.getLogger("recipeLogger");
        logger.info("Logging for the first Time!");
        logger.warn("A warning to be had");
        logger.error("This is an error!");

        Logger rollingLogger = LoggerFactory.getLogger("rollingLogger");
        for (int i =0;i < 5000;i++) {
            rollingLogger.info("Logging for an event with :"+i);
        }

logging.properties file

handlers = java.util.logging.FileHandler

recipeLogger.level=INFO

.level=ALL

java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.pattern=ImportantApplication%d.log
java.util.logging.FileHandler.limit=50000
java.util.logging.FileHandler.count=4

它是如何工作的

要控制日志文件的大小,请配置 java.util.logging 框架并指定滚动日志文件。选择“滚动日志文件”选项会将最新信息保存在 ImportantApplication0.log 中。越旧的信息将保存在 ImportantApplication1.log、ImportantApplication2.log 中,依此类推。当 ImportantApplication0.log 填充到您指定的限制(在此示例中为 50,000 字节)时,其名称将旋转到 ImportantApplicationLog1.log,其他文件的名称也将类似地向下旋转。要维护的日志文件的数量由 Java . util . logging . file handler . count 属性决定,该属性在本例中设置为 4。

logging.properties 文件首先定义 java.util.logging 框架将使用的处理程序。处理程序是负责记录消息的对象。在配方中指定了 FileHandler,它将消息记录到文件中。其他可能的处理程序有 ConsoleHandler(记录到 system.output 设备)、SocketHandler(记录到套接字)和 MemoryHandler(将日志保存在内存的循环缓冲区中)。还可以通过创建一个扩展处理程序抽象类的类来指定自己的处理程序实现。

接下来,定义日志记录级别。在日志框架中,有一个独立日志对象的概念。记录器可以有不同的配置(例如,不同的日志记录级别),并且可以在日志文件中识别。该示例将 recipeLogger 的级别配置为 info,而根日志记录器的级别为 ALL(Java . util . logging 框架中的根日志记录器在属性前没有任何前缀)。

logging.properties 文件的下一节定义了 FileHandler 配置。格式化程序指示如何将日志信息写入磁盘。simpleFormatter 以纯文本格式写入信息,其中一行表示日期和时间,一行表示日志记录级别,还有要记录的消息。格式化程序的另一个默认选项是 XMLFormatter,它将为每个日志事件创建包含日期、时间、记录器名称、级别、线程和消息信息的 XML 标记。您可以通过扩展 Formatter 抽象类来创建自定义格式化程序。

在格式化程序之后,定义了 fileHandler 模式。这指定了日志文件的文件名和位置(滚动日志号[0∾4]替换了%d)。Limit 属性定义日志在翻转之前可以有多少字节(50,000 字节 50kb)。计数定义了要保留的日志文件的最大索引(在本例中是 4)。

注意

伐木成本可能很高;如果您正在记录大量信息,您的 Java 程序将开始消耗内存(因为 java.util.logging 框架将尝试将所有需要写入磁盘的信息保存在内存中,直到可以刷新这些信息)。如果 java.util.logging 框架写入日志文件的速度跟不上创建日志条目的速度,您将会遇到内存不足的错误。最好的方法是只记录必要的信息,如果需要的话,在写出调试日志消息之前查看 Logger.isDebugEnabled()。可以从日志记录配置文件中更改日志记录级别。

9-11.记录异常

从前面的菜谱中,您学习了如何捕捉异常以及如何记录信息。这个菜谱会把这两个菜谱放在一起。

问题

您希望在日志文件中记录异常。

解决办法

将应用配置为使用 SLF4J。利用 try/catch 块在错误日志中记录异常。在下面的示例中,SLF4J 记录器用于记录异常处理程序中的消息。

static Logger rootLogger = LoggerFactory.getLogger("");
private void start() {
    loadLoggingConfiguration();
    Thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e) -> {
        rootLogger.error("Error in thread "+t+" caused by ",e);
    });

    int c = 20/0;
}
private void loadLoggingConfiguration() {
    FileInputStream ins = null;
    try {
        ins = new FileInputStream(new File("logging.properties"));
        LogManager.getLogManager().readConfiguration(ins);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

它是如何工作的

该示例演示了如何结合使用 UncaughtExceptionHandler 和 SLF4J 来将异常记录到日志文件中。在记录异常时,最好包括显示异常抛出位置的栈跟踪。在示例中,一个线程包含一个 UncaughtExceptionHandler,它利用了一个包含日志记录器的 lambda 表达式。记录器用于将任何捕获的异常写入日志文件。

注意

如果异常被重复抛出,JVM 倾向于停止在异常对象中填充栈跟踪。这样做是出于性能原因,因为检索相同的栈跟踪会变得很昂贵。如果发生这种情况,您将会看到一个没有记录栈跟踪的异常。当发生这种情况时,检查日志以前的条目,看看是否抛出了相同的异常。如果以前抛出过相同的异常,则完整的栈跟踪将出现在该异常的第一个日志实例中。

9-12.使用统一 JVM 日志记录器进行日志记录

问题

您希望执行 JVM 进程的日志记录,并且希望对日志记录进行细粒度的控制。

解决办法

利用作为 Java 9 的一部分添加的统一 JVM 记录器实用程序。在下面的解决方案中,JVM logger 实用程序被配置为执行日志记录并指向操作系统上的文件。

要启动日志记录,请打开命令提示符或终端,并执行以下语句:

java -Xlog:all:file=test.txt:time,level

该语句将配置 JVM 将所有标记记录到一个名为 test.txt 的文件中。下一个例子演示了如何使用“gc”记录标记,使用“trace”级别记录标记,使用“uptime”修饰记录到 stdout。

java –Xlog:gc=trace:uptime

它是如何工作的

Java 9 的发布增强了 JVM 的日志功能,允许一个统一的系统提供细粒度的控制。在过去,记录 JVM 系统级组件可能会成为一项耗时的任务,因为很难查明许多问题的根本原因。更新的日志记录工具提供了以下功能:

  • 用于记录各种 JVM 进程的常见命令行选项

  • 标签分类

  • 日志记录级别之间的差异

  • 记录到文件的能力

  • 文件旋转能力

  • 动态配置

要配置 JVM 日志记录,请使用–Xlog 标志执行 java.exe,将选项附加到用冒号[:]分隔的标志。如果您希望为 JVM 的单次运行执行日志记录,请在调用 Java 应用时包含–Xlog 标志。

–Xlog 标志有几个可用选项,以下列格式指示记录“内容”和“位置”:

-Xlog[:option=<what:level>:<output>:<decorators>:<output-options>]

请注意,在该格式中,您可以指定–Xlog 而不带任何选项,以指示应该记录所有标记,并且所有记录级别都将转到 stdout。在该解决方案中,我们看到,要配置所有标记的日志记录,您还可以指定“all”选项。省略部分将默认使用“info”级别的标签集“all”忽略将默认为“信息”表 9-2 中列出了可用的装饰器,省略它们会默认为“正常运行时间”、“级别”、“标签”

表 9-2。Xlog 装饰者
|

装饰者

|

描述

|
| --- | --- |
| 时间 | 当前时间和日期(ISO-8601) |
| 正常运行时间 | 自 JVM 启动以来超过的时间量(秒和毫秒) |
| 时间英里 | System.currentTimeMillis()输出 |
| Uptimemillis | 自 JVM 启动以来已超过毫秒 |
| 时间纳米 | System.nanoTime()输出 |
| 上升趋势 | 自从 JVM 启动以来,纳秒被超越了 |
| Pid | 进程标识符 |
| 每日三次 | 线程标识符 |
| 水平 | 相关日志消息级别 |
| 标签 | 关联日志消息标记 |

支持三种类型的输出:标准输出、标准错误和文本文件。通过指定输出选项,可以将输出配置为旋转文件、限制文件大小等。可能的输出选项包括:

  • 文件计数 =

  • 文件大小 =

  • 参数=值

可以通过 jcmd 诊断命令实用程序在运行时控制日志 API。命令行中可用的所有选项也可以通过该实用程序获得。

注意

使用–Xlog:help 开关可以获得关于 JVM 日志记录实用程序的帮助。该开关将打印用法语法和可用的标签、级别、装饰符和示例。

摘要

在这一节中,我们看了一下应用开发中最重要的阶段之一,异常处理。这些小节讨论了如何处理单个和多个异常,以及如何记录这些异常。有许多成熟的日志 API 可用于 JVM,我们在本章中讨论了 SLF4J。最后,我们看了一下 Java 9 中引入的统一 JVM 日志记录过程。

十、并发

并发是现代计算机编程中最难处理的话题之一;理解并发性需要抽象思维的能力,调试并发问题就像试图通过航位推算驾驶飞机一样。即便如此,随着 Java 的现代发布,编写无 bug 的并发代码变得更加容易(也更容易访问)。

并发是一个程序同时执行不同(或相同)指令的能力。一个被称为并发的程序能够被分割并在多个 CPU 上运行。通过编写并发程序,您可以利用当今的多核 CPU。您甚至可以看到 I/O 密集型单核 CPU 的优势。

在这一章中,我们介绍了并发任务最常见的需求——从运行后台任务到将计算拆分成工作单元。在这一章中,你会发现在 Java 中实现并发性的最新方法。

10-1.启动后台任务

问题

您有一个需要在主线程之外运行的任务。

解决办法

创建一个包含需要在不同线程中运行的任务的类实现。在任务实现类中实现一个 Runnable 接口,启动一个新线程。在下面的示例中,一个计数器用于模拟活动,因为一个单独的任务在后台运行。

注意

这个例子中的代码可以被重构以利用方法引用(参见第六章),而不是为新的线程实现创建一个内部类。然而为了清楚起见,匿名内部类已经被显示。

private void someMethod()  {
      Thread backgroundThread = new Thread(new Runnable() {
          public void run() {
              doSomethingInBackground();
          }
      },"Background Thread");

      System.out.println("Start");
      backgroundThread.start();
      for (int i= 0;i < 10;i++) {
          System.out.println(Thread.currentThread().getName()+": is counting "+i);
      }

      System.out.println("Done");
  }

  private void doSomethingInBackground() {
      System.out.println(Thread.currentThread().getName()+
       ": is Running in the background");
  }

如果代码执行多次,输出应该会不时地不同。后台线程将单独执行,因此它的消息在每次运行的不同时间打印。

如果使用 lambda 表达式,创建后台线程的相同代码可以编写如下:

Thread backgroundThread = new Thread(this::doSomethingInBackground, "Background Thread");

它是如何工作的

Thread 类允许在不同于当前线程的新线程(执行路径)中执行代码。线程构造函数需要一个实现 Runnable 接口的类作为参数。Runnable 接口只需要实现一个方法:public void run()。因此,它是一个函数接口,方便了 lambda 表达式的使用。当调用 Thread.start()方法时,它将依次创建新线程并调用 Runnable 的 run()方法。

JVM 中有两种类型的线程:用户和守护进程。用户线程会一直执行,直到它们的 run()方法完成,而守护线程可以在应用需要退出时终止。如果 JVM 中只有守护线程在运行,应用就会退出。当您开始创建多线程应用时,您必须意识到这些差异,并了解何时使用每种类型的线程。

通常,守护线程会有一个不完整的可运行接口;例如 while(真)循环。这允许这些线程在程序的整个生命周期中定期检查或执行某个条件,并在程序完成执行时被丢弃。相反,用户线程在活动时会执行并阻止程序终止。如果您碰巧有一个程序没有像预期的那样关闭和/或退出,您可能希望检查正在运行的线程。

若要将线程设置为守护进程线程,请在调用 thread.start()方法之前使用 thread.setDaemon(true)。默认情况下,线程实例被创建为用户线程类型。

注意

这个食谱展示了创建和执行一个新线程的最简单的方法。创建的新线程是一个用户线程,这意味着应用在主线程和后台线程都执行完之前不会退出。

10-2.更新(和迭代)地图

问题

您需要从多个线程更新一个 Map 对象,并且希望确保更新不会破坏 Map 对象的内容,并且 Map 对象始终处于一致的状态。您还希望在其他线程更新 Map 对象时遍历(查看)Map 对象的内容。

解决办法

使用 ConcurrentMap 更新映射条目。以下示例创建了 1,000 个线程。然后每个线程同时尝试修改映射。主线程等待一秒钟,然后继续遍历地图(即使其他线程仍在修改地图):

Set<Thread> updateThreads = new HashSet<>();

private void startProcess() {
    ConcurrentMap<Integer,String> concurrentMap = new ConcurrentHashMap<>();
    for (int i =0;i < 1000;i++) {
        startUpdateThread(i, concurrentMap);
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    concurrentMap.entrySet().stream().forEach((entry) -> {
        System.out.println("Key :"+entry.getKey()+" Value:"+entry.getValue());
    });

    updateThreads.stream().forEach((thread) -> {
        thread.interrupt();
    });
}

Random random = new Random();
private void startUpdateThread(int i, final ConcurrentMap<Integer, String> concurrentMap) {
    Thread thread = new Thread(() -> {
        while (!Thread.interrupted()) {
            int randomInt = random.nextInt(20);
            concurrentMap.put(randomInt, UUID.randomUUID().toString());
        }
    });
    thread.setName("Update Thread "+i);
    updateThreads.add(thread);
    thread.start();
}

它是如何工作的

为了以并发方式在哈希表上执行工作,ConcurrentHashMap 允许多个线程同时安全地修改哈希表。ConcurrentHashMap 是一个哈希表,支持检索的完全并发性和更新的可调预期并发性。在本例中,1000 个线程在很短的时间内对地图进行了修改。ConcurrentHashMap 迭代器以及在 ConcurrentHashMap 上生成的流允许对其内容进行安全迭代。当使用 ConcurrentMap 的迭代器时,您不必担心在对其进行迭代时锁定 ConcurrentMap 的内容(并且它不会抛出 ConcurrentModificationExceptions)。

有关新添加方法的完整列表,请参考位于docs . Oracle . com/javase/9/docs/API/Java/util/concurrent/concurrent hashmap . html的在线文档。

注意

ConcurrentMap 迭代器虽然是线程安全的,但并不保证在迭代器创建后会看到条目被添加/更新。

10-3.仅当键不存在时,才在映射中插入键

问题

应用中的映射会不断更新,如果键不存在,您需要将键/值对放入其中。因此,您需要检查这个键是否存在,并且您需要确保其他线程不会同时插入同一个键。

解决办法

使用 concurrent map . puti absent()方法,可以确定映射是否被自动修改。例如,下面的代码使用方法在一个步骤中进行检查和插入,从而避免了并发问题:

private void start() {
    ConcurrentMap<Integer, String> concurrentMap = new ConcurrentHashMap<>();
    for (int i = 0; i < 100; i++) {
        startUpdateThread(i, concurrentMap);
    }

    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    concurrentMap.entrySet().stream().forEach((entry) -> {
        System.out.println("Key :" + entry.getKey() + " Value:" + entry.getValue());
    });

}

Random random = new Random();

private void startUpdateThread(final int i, final ConcurrentMap<Integer, String> concurrentMap) {
    Thread thread = new Thread(() -> {
        int randomInt = random.nextInt(20);
        String previousEntry = concurrentMap.putIfAbsent(randomInt, "Thread # " + i + " has made it!");
        if (previousEntry != null) {
            System.out.println("Thread # " + i + " tried to update it but guess what, we're too late!");
        } else {
            System.out.println("Thread # " + i + " has made it!");  
        }
    });
    thread.start();
}

当运行程序时,一些条目将被成功插入,而另一些则不会,因为键已经被另一个线程插入。注意,在这个例子中,startUpdateThread()接受一个最终的 int i 参数。将方法参数标记为 final 可确保该方法不会更改变量 I 的值。如果 I 的值在方法内部发生了更改,则从方法外部看不到这种更改。

它是如何工作的

并发更新地图是困难的,因为它涉及两个操作:一个检查然后行动类型的操作。首先,必须检查映射以查看其中是否已经存在一个条目。如果条目不存在,可以将键和值放入映射中。另一方面,如果键存在,则检索该键的值。为此,我们使用 ConcurrentMap 的 putIfAbsent 原子操作。这确保要么键存在,因此值不会被覆盖,要么键不存在,因此值被设置。对于 ConcurrentMap 的 JDK 实现,如果键没有值,putIfAbsent()方法将返回 null,如果键有值,则返回当前值。通过断言 putIfAbsent()方法返回 null,可以确保操作成功,并且在映射中创建了一个新条目。

在某些情况下,putIfAbsent()的执行效率可能不高。例如,如果结果是一个大型数据库查询,那么一直执行数据库查询,然后调用 putIfAbsent()就不会有效率。在这种情况下,您可以首先调用 map 的 containsKey()方法来确保该键不存在。如果它不存在,那么用昂贵的数据库查询调用 putIfAbsent()。可能有可能 putIfAbsent()没有放入条目,但是这种类型的检查减少了潜在的昂贵的值创建的数量。

请参见以下代码片段:

keyPresent = concurrentMap.containsKey(randomInt);
             if (!keyPresent)  {
               concurrentMap.putIfAbsent(randomInt, "Thread # " + i + " has made it!");
         }

在这段代码中,第一个操作是检查该键是否已经在映射中。如果是,它不会执行 putIfAbsent()操作。如果这个键不存在,我们可以继续执行 putIfAbsent()操作。

如果从不同的线程访问 map 的值,应该确保这些值是线程安全的。这在使用集合作为值时最为明显,因为它们可以从不同的线程访问。确保主映射是线程安全的将防止对映射的并发修改。然而,一旦获得了对映射值的访问权,就必须围绕映射值进行良好的并发实践。

注意

ConcurrentMaps 不允许空键,这与其非线程安全的表亲 HashMap(允许空键)不同。

10-4.遍历变化的集合

问题

您需要迭代集合中的每个元素。但是,其他线程会不断更新集合。

解决方案 1

通过使用 CopyOnWriteArrayList,您可以安全地循环访问集合,而不必担心并发性。在下面的解决方案中,startUpdatingThread()方法创建了一个新线程,它主动更改传递给它的列表。当 startUpdatingThread()修改列表时,使用 stream forEach()函数同时迭代它。

private void copyOnWriteSolution() {
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
    startUpdatingThread(list);
    list.stream().forEach((element) -> {
        System.out.println("Element :" + element);
    });
    stopUpdatingThread();

}

解决方案 2

使用 synchronizedList()允许我们原子地改变集合。此外,synchronizedList()提供了一种在遍历列表时安全同步列表的方法(在流中完成)。例如:

private void synchronizedListSolution() {
    final List<String> list = Collections.synchronizedList(new ArrayList<String>());
    startUpdatingThread(list);
    synchronized (list) {
        list.stream().forEach((element) -> {
            System.out.println("Element :" + element);
        });
    }
    stopUpdatingThread();
}

它是如何工作的

Java 附带了许多并发收集选项。使用哪个集合取决于应用上下文中读操作与写操作的比较。如果与读取相比,写入发生在远处和中间,使用 copyOnWriteArrayList 实例是最有效的,因为它不会阻止 (stop)其他线程读取列表,并且迭代是线程安全的(迭代时不会引发 ConcurrentModificationException)。如果有相同数量的写入和读取,使用 SynchronizedList 是首选。

在解决方案 1 中,当您遍历列表时,CopyOnWriteArrayList 正在更新。因为这个配方使用了 CopyOnWriteArrayList 实例,所以在遍历集合时不需要担心线程安全(正如在这个配方中使用 stream 所做的那样)。值得注意的是,CopyOnWriteArrayList 在遍历它时提供了一个及时的快照。如果另一个线程在你迭代的时候修改了这个列表,那么修改后的列表在迭代的时候是不可见的。

注意

正确锁定取决于所使用的集合类型。使用 Collections.synchronized 返回的任何集合都可以通过集合本身锁定(synchronized(collection instance))。然而,一些更有效(更新)的并发集合(如 ConcurrentMap)不能以这种方式使用,因为它们的内部实现并不锁定对象本身。

解决方案 2 创建一个同步列表,它是通过使用 Collections helper 类创建的。Collection.synchronizedList()方法将 List 对象(可以是 ArrayList、LinkedList 或另一个 List 实现)包装到一个列表中,以同步对列表操作的访问。每次需要迭代一个列表时(无论是通过使用流、for 循环还是迭代器),都必须意识到列表迭代器的并发性。对 CopyOnWriteArrayList 进行迭代是安全的(如 Javadoc 中所指定的),但是 synchronizedList 迭代器必须手动同步(也在 collections . synchronized list . list 迭代器 Javadoc 中指定)。在这个解决方案中,列表可以在 synchronized(list)块中安全地迭代。当对列表进行同步时,在同步(列表)块完成之前,不会发生读取/更新/其他迭代。

10-5.协调不同的收藏

问题

您需要同时修改不同但相关的集合,并且希望确保在这些修改完成之前,其他线程看不到这些修改。

解决方案 1

通过对主体集合进行同步,可以保证集合可以同时更新。在下面的示例中,fulfillOrder 方法需要检查要履行的订单的库存,如果有足够的库存来履行订单,它需要将订单添加到 customerOrders 列表中。fulfillOrder()方法同步 inventoryMap,并在完成同步块之前修改 inventoryMap 和 customerOrders 列表。

    private boolean fulfillOrder(String itemOrdered, int quantityOrdered, String customerName) {
        synchronized (inventoryMap) {
            int currentInventory  = 0;
            if (inventoryMap != null) {
                currentInventory = inventoryMap.get(itemOrdered);
            }
            if (currentInventory < quantityOrdered) {
                System.out.println("Couldn't fulfill order for "+customerName+" not enough "+itemOrdered+" ("+quantityOrdered+")");
                return false; // sorry, we sold out
            }
            inventoryMap.put(itemOrdered,currentInventory - quantityOrdered);
            CustomerOrder order = new CustomerOrder(itemOrdered, quantityOrdered, customerName);
            customerOrders.add(order);
            System.out.println("Order fulfilled for "+customerName+" of "+itemOrdered+" ("+quantityOrdered+")");
            return true;
        }
}

    private void checkInventoryLevels() {
        synchronized (inventoryMap) {
            System.out.println("------------------------------------");
            inventoryMap.entrySet().stream().forEach((inventoryEntry) -> {
                System.out.println("Inventory Level :"+inventoryEntry.getKey()+" "+inventoryEntry.getValue());
            });
            System.out.println("------------------------------------");
        }
    }

    private void displayOrders() {
        synchronized (inventoryMap) {
            customerOrders.stream().forEach((order) -> {
                System.out.println(order.getQuantityOrdered()+" "+order.getItemOrdered()+" for "+order.getCustomerName());
            });
        }
    }

解决方案 2

使用可重入锁,可以防止多个线程访问代码的同一个关键区域。在这个解决方案中,inventoryLock 是通过调用 inventoryLock.lock()获得的。任何试图获取 inventoryLock 锁的其他线程都必须等待,直到 inventoryLock 锁被释放。在 fulfillOrder()方法结束时(在 finally 块中),通过调用 inventoryLock.unlock()方法来释放 inventoryLock:

Lock inventoryLock = new ReentrantLock();
private boolean fulfillOrder(String itemOrdered, int quantityOrdered, String customerName) {
      try {
          inventoryLock.lock();
          int currentInventory = inventoryMap.get(itemOrdered);
          if (currentInventory < quantityOrdered) {
              System.out.println("Couldn't fulfill order for " + customerName +
                 " not enough " + itemOrdered + " (" + quantityOrdered + ")");
              return false; // sorry, we sold out
          }
          inventoryMap.put(itemOrdered, currentInventory - quantityOrdered);
          CustomerOrder order = new CustomerOrder(itemOrdered, quantityOrdered, customerName);
          customerOrders.add(order);
          System.out.println("Order fulfilled for " + customerName + " of " +
                 itemOrdered + " (" + quantityOrdered + ")");
          return true;
      } finally {
          inventoryLock.unlock();
      }
  }

  private void checkInventoryLevels() {
      try {
          inventoryLock.lock();
          System.out.println("------------------------------------");
          inventoryMap.entrySet().stream().forEach((inventoryEntry) -> {
              System.out.println("Inventory Level :" + inventoryEntry.getKey() + " " + inventoryEntry.getValue());
          });
          System.out.println("------------------------------------");
      } finally {
          inventoryLock.unlock();
      }
  }

  private void displayOrders() {
      try {
          inventoryLock.lock();
          customerOrders.stream().forEach((order) -> {
              System.out.println(order.getQuantityOrdered() + " " +
               order.getItemOrdered() + " for " + order.getCustomerName());
          });
      } finally {
          inventoryLock.unlock();
      }
  }

它是如何工作的

如果您有不同的结构需要同时修改,您需要确保这些结构是自动更新的。一个原子操作指的是一组可以整体执行或者根本不执行的指令。原子操作只有在完成时才对程序的其余部分可见。

在解决方案 1 中(自动修改 inventoryMap 和 customerOrders 列表),您选择一个将锁定的“主体”集合(inventoryMap)。通过锁定主体集合,可以保证如果另一个线程试图锁定同一个主体集合,它将不得不等待,直到当前正在执行的线程释放对该集合的锁定。

注意

请注意,即使 displayOrders 不使用 inventoryMap,您仍然在它上面进行同步(在解决方案 1 中)。因为 inventoryMap 是主集合,所以即使是在辅助集合上完成的操作也仍然需要受到主集合同步的保护。

解决方案 2 更加明确,它提供了一个独立的锁,用于协调原子操作,而不是选择一个主体集合。锁定指的是 JVM 限制某些代码路径只能由一个线程执行的能力。线程试图获取锁(例如,锁是由 ReentrantLock 实例提供的,如示例所示),并且一次只能将锁授予一个线程。如果其他线程试图获取同一个锁,它们将被挂起(等待),直到锁可用。当当前持有该锁的线程释放它时,该锁变得可用。当一个锁被释放时,它可以被一个(且只有一个)等待该锁的线程获取。

默认情况下,锁并不“公平”换句话说,不保持请求锁的线程的顺序;这允许 JVM 中非常快速的锁定/解锁实现,并且在大多数情况下,使用不公平的锁通常是可以的。对于竞争非常激烈的锁,如果需要平均分配锁(使其公平),可以通过设置锁的 setFair 属性来实现。

在解决方案 2 中,调用 inventoryLock.lock()方法将获得锁并继续,或者将暂停执行(等待)直到可以获得锁。一旦获得锁,其他线程将无法在锁定的块中执行。在代码块的末尾,通过调用 inventoryLock.unlock()释放锁。

在使用锁对象(ReentrantLock、ReadLock 和 WriteLock)时,通常的做法是通过 try/finally 子句来使用这些锁对象。打开 try 块后,第一条指令是调用 lock.lock()方法。这保证了执行的第一条指令是获取锁。锁的释放(通过调用 lock.unlock())是在匹配的 finally 块中完成的。如果在获取锁时发生 RuntimeException,在 finally 子句中解锁可以确保不会“保留”锁并阻止其他线程获取它。

ReentrantLock 对象的使用提供了 synchronized 语句所没有的附加特性。例如,ReentrantLock 具有 tryLock()函数,该函数仅在没有其他线程拥有锁的情况下尝试获取锁(该方法不会让调用线程等待)。如果另一个线程持有锁,则该方法返回 false,但继续执行。最好使用 synchronized 关键字进行同步,并且只在需要 ReentrantLock 的特性时才使用它。有关 ReentrantLock 提供的其他方法的更多信息,请访问docs . Oracle . com/javase/9/docs/API/Java/util/concurrent/locks/reentrant lock . html

小费

虽然这只是一本食谱,而且正确的线程技术也涵盖了它们自己的内容,但是提高对死锁的认识是很重要的。死锁发生在涉及两个锁时(并且在另一个线程中以相反的顺序获得)。避免死锁的最简单的方法是避免让锁“逃脱”这意味着锁在被获取时不应执行调用其他方法的代码,这些方法可能会获取不同的锁。如果不可能,在调用这样的方法之前释放锁。

应该注意的是,引用一个或两个集合的任何操作都需要由同一个锁来保护。依赖一个集合的结果来查询第二个集合的操作需要原子地执行;它们需要作为一个单元来完成,在操作完成之前,任何一个集合都不能改变。

10-6.将工作拆分到不同的线程中

问题

您的工作可以拆分到单独的线程中,并且希望最大限度地利用可用的 CPU 资源。

解决办法

使用 ThreadpoolExecutor 实例,它允许我们将任务分成离散的单元。在下面的示例中,创建了一个 BlockingQueue,其中包含一个 Runnable 对象。然后将它传递给 ThreadPoolExecutor 实例。然后通过调用 prestartAllCoreThreads()方法来初始化和启动 ThreadPoolExecutor。接下来,通过调用 shutdown()方法,然后调用 awaitTermination()方法,执行一次有序的关闭,其中执行所有以前提交的任务:

private void start() throws InterruptedException {
    BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    for (int i =0;i < 10;i++) {
        final int localI = i;
        queue.add((Runnable) () -> {
            doExpensiveOperation(localI);
        });
    }
    ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,1000,
            TimeUnit.MILLISECONDS, queue);
    executor.prestartAllCoreThreads();
    executor.shutdown();
    executor.awaitTermination(100000,TimeUnit.SECONDS);

    System.out.println("Look ma! all operations were completed");
}

它是如何工作的

ThreadPoolExecutor 由两个组件组成:要执行的任务队列,以及告诉如何执行任务的执行器。队列中填充了 Runnable 对象,run()方法包含要执行的代码。

ThreadPoolExecutor 使用的队列是 BlockingQueue 接口的实现者。BlockingQueue 接口表示一个队列,如果队列中没有元素,队列的使用者将在该队列中等待(被挂起)。这是 ThreadPoolExecutor 高效工作所必需的。

第一步是用需要并行执行的任务填充队列。这是通过调用队列的 add()方法并向其传递一个实现 Runnable 接口的类来实现的。一旦完成,执行器就被初始化了。

ThreadPoolExecutor 构造函数有许多参数选项;解决方案中使用的是最简单的。表 10-1 有每个参数的描述。

表 10-1。ThreadPoolExecutor 的参数
|

参数

|

描述

|
| --- | --- |
| 最小雇佣数量 | 提交任务时创建的最小线程数 |
| MaximumPoolSize | 执行器将创建的最大线程数 |
| KeepAliveTime | 等待线程在被释放之前等待工作的时间(只要活动线程的数量仍然大于 CorePoolSize) |
| 时间单元 | 表示 KeepAliveTime 的单位(即 TimeUnit。秒,时间单位。毫秒) |
| WorkQueue(工作队列) | 包含将由执行器处理的任务的阻塞队列 |

在 ThreadPoolExecutor 初始化之后,调用 prestartAllCore Threads()。此方法通过创建 CorePoolSize 中指定数量的线程来“预热”ThreadPoolExecutor,并在队列不为空时主动开始消耗队列中的任务。

调用 ThreadPoolExecutor 的 shutdown()方法,等待所有任务完成。通过调用此方法,ThreadPoolExecutor 被指示不接受来自队列的新事件(以前提交的事件将完成处理)。这是有序终止 ThreadPoolExecutor 的第一步。调用 awaitTermination()方法等待 ThreadPoolExecutor 中的所有任务完成。该方法将强制主线程等待,直到 ThreadPoolExecutor 队列中的所有 Runnables 都执行完毕。在所有的 Runnables 都被执行后,主线程将被唤醒并继续执行。

注意

需要正确配置 ThreadPoolExecutor 以最大限度地利用 CPU。对于一个执行器来说,最有效的线程数量取决于提交的任务类型。如果任务是 CPU 密集型的,那么拥有一个具有当前内核数量的执行器将是理想的。如果任务是 I/O 密集型的,执行器应该拥有比当前线程核心数更多的线程。I/O 绑定越多,线程数量就越多。

10-7.协调线程

问题

您的应用要求两个或多个线程协调一致地工作。

解决方案 1

通过线程同步的等待/通知,可以协调线程。在这个解决方案中,主线程等待 objectToSync 对象,直到数据库加载线程完成执行。一旦数据库加载线程完成,它就通知 objectToSync,等待它的任何人都可以继续执行。将订单加载到我们的系统时,也会发生同样的过程。主线程等待 objectToSync,直到 orders-loading 线程通过调用 objectToSync.notify()方法通知 objectToSync 继续。在确保装载了库存和订单之后,主线程执行 processOrder()方法来处理所有订单。

private final Object objectToSync = new Object();

private void start() {
    loadItems();

    Thread inventoryThread = new Thread(() -> {
        System.out.println("Loading Inventory from Database...");
        loadInventory();
        synchronized (objectToSync) {
            objectToSync.notify();
        }
    });

    synchronized (objectToSync) {
        inventoryThread.start();
        try {
            objectToSync.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    Thread ordersThread = new Thread(() -> {

        System.out.println("Loading Orders from XML Web service...");
        loadOrders();
        synchronized (objectToSync) {
            objectToSync.notify();

        }
    });

    synchronized (objectToSync) {
        ordersThread.start();
        try {
            objectToSync.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    processOrders();
}

解决方案 2

您可以使用 CountDownLatch 对象控制主线程何时继续。在下面的代码中,创建了一个初始值为 2 的 CountDownLatch 然后创建并启动两个线程,一个用于装载库存,另一个用于装载订单信息。当两个线程都完成执行时,它们调用 CountDownLatch 的 countDown()方法,该方法将闩锁的值减 1。主线程会一直等到 CountDownLatch 达到 0,然后继续执行。

CountDownLatch latch = new CountDownLatch(2);

private void start() {
    loadItems();

    Thread inventoryThread = new Thread(() -> {
        System.out.println("Loading Inventory from Database...");
        loadInventory();
        latch.countDown();
    });

    inventoryThread.start();

    Thread ordersThread = new Thread(() -> {
        System.out.println("Loading Orders from XML Web service...");
        loadOrders();
        latch.countDown();
    });

    ordersThread.start();

    try {
        latch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    processOrders();

}

解决方案 3

通过使用 Thread.join(),您可以等待线程完成执行。下面的例子有一个装载库存的线程和另一个装载订单的线程。一旦每个线程被启动,对 inventoryThread.join()的调用将使主线程在继续之前等待 inventoryThread 完成执行。

private void start() {
    loadItems();

    Thread inventoryThread = new Thread(() -> {
        System.out.println("Loading Inventory from Database...");
        loadInventory();
    });

    inventoryThread.start();
    try {
        inventoryThread.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    Thread ordersThread = new Thread(() -> {
        System.out.println("Loading Orders from XML Web service...");
        loadOrders();
    });

    ordersThread.start();
    try {
        ordersThread.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    processOrders();
}

它是如何工作的

Java 中有许多协调线程的方法,这些协调工作依赖于让线程等待的概念。当线程等待时,它会暂停执行(它不会继续执行下一条指令,并从 JVM 的线程调度器中删除)。如果一个线程正在等待,那么可以通过通知它来唤醒它。在 Java 的并发行话中,单词 notify 意味着线程将恢复执行(JVM 将线程添加到线程调度器中)。因此,在线程协调的自然过程中,最常见的事件序列是一个主线程等待,然后一个辅助线程通知主线程继续(或唤醒)。即便如此,等待线程仍有可能被其他事件中断。当线程被中断时,它不会继续执行下一条指令,而是抛出 InterruptedException,这是一种发出信号的方式,即使线程正在等待某件事情发生,也有一些其他事件发生了,需要线程的注意。下面的例子更好地说明了这一点:

BlockingQueue queue                                               = new LinkedBlockingQueue();
while (true) {
    synchronized (this) {
        Object itemToProcess = queue.take();
        processItem (itemToProcess);
    }
}

如果您查看前面的代码,执行该代码的线程将永远不会终止,因为它会永远循环下去,并等待一个项目被处理。如果队列中没有项目,主线程会一直等待,直到另一个线程将项目添加到队列中。您不能优雅地关闭前面的代码(特别是如果运行循环的线程不是守护线程)。

BlockingQueue queue = new LinkedBlockingQueue();
while (true) {
    synchronized (this) {
        Object itemToProcess = null;
        try {
            itemToProcess = queue.take();
        } catch (InterruptedException e) {
            return;
        }
        processItem (itemToProcess);
    }
}

新代码现在具有“逃离”无限循环的能力。从另一个线程中,可以调用 thread.interrupt(),它抛出 InterruptedException,然后由主线程的 catch 子句捕获。可以在该子句中退出无限循环。

中断异常是一种向等待(或休眠)线程发送额外信息的方式,以便它们可以处理不同的场景(例如,有序的程序关闭)。因此,每个将线程状态更改为 sleep/wait 的操作都必须由 try/catch 块包围,该块可以捕获 InterruptedException。在这种情况下,异常(InterruptedException)并不是真正的错误,而是一种在线程之间发出信号的方式,表明发生了需要注意的事情。

解决方案 1 展示了最常见(最古老)的协调形式。该解决方案要求让一个线程等待并暂停执行,直到另一个线程通知(或唤醒)该线程。

要使解决方案 1 起作用,发起线程需要获得一个锁。这个锁将成为“电话号码”,在这个号码上另一个线程可以通知发起线程醒来。在发起线程获得锁(电话号码)后,它继续等待。只要调用 wait()方法,锁就会被释放,从而允许其他线程获得同一个锁。然后,辅助线程继续获取锁(电话号码),然后通知(实际上,这就像拨打唤醒电话)发起线程。通知之后,发起线程恢复执行。

在解决方案 1 的代码中,锁是一个标识为 objectToSync 的虚拟对象。实际上,锁正在等待和通知的对象可以是 Java 中任何有效的实例对象;例如,我们可以使用 this 引用让主线程等待(在线程中,我们可以使用 Recipe 10_7_1.this 变量引用来通知主线程继续)。

使用这种技术的主要优点是控制等待谁以及何时通知的明确性(以及通知所有正在等待同一对象的线程的能力;参见下面的提示)。

小费

多个线程可以等待同一个锁(同一个电话号码被唤醒)。当一个辅助线程调用 notify 时,它将唤醒一个“等待”线程(关于哪个被唤醒是不公平的)。有时你需要通知所有的线程;您可以调用 notifyAll()方法,而不是调用 notify()方法。这主要用于准备许多线程来执行一些工作,但工作尚未完成设置的情况。

解决方案 2 使用更现代的通知方法,因为它涉及 CountDownLatch。设置时,指定闩锁的“计数”数。然后,主线程将通过调用 CountDownLatch 的 await()方法来等待(停止执行),直到闩锁倒计时到 0。当 latch 达到 0 时,主线程将被唤醒并继续执行。当工作线程完成时,调用 latch.countdown()方法,这将减少闩锁的当前计数值。如果 latch 的当前值达到 0,等待 CountDownLatch 的主线程将被唤醒并继续执行。

使用 CountDownLatches 的主要优点是可以同时生成多个任务,然后等待所有任务完成。(在解决方案示例中,我们不需要等到一个或另一个线程完成后再继续;都是启动的,当 latch 为 0 时,主线程继续。)

解决方案 3 提供了一种解决方案,在这种方案中,我们可以访问我们想要等待的线程。对于主线程来说,调用次线程的 join()方法就行了。那么主线程将等待(停止执行)直到次线程完成。

这种方法的优点是它不需要辅助线程知道任何同步机制。只要辅助线程终止执行,主线程就可以等待它们。

10-8.创建线程安全的对象

问题

您需要创建一个线程安全的对象,因为它将被多个线程访问。

解决方案 1

使用同步的 getters 和 setters,保护会改变状态的关键区域。在下面的示例中,创建了一个对象,其中的 getters 和 setters 针对每个内部变量进行了同步。通过使用同步(this)锁来保护关键区域:

class CustomerOrder {
    private String itemOrdered;
    private int quantityOrdered;
    private String customerName;

    public CustomerOrder() {

    }

    public double calculateOrderTotal (double price) {
        synchronized (this) {
            return getQuantityOrdered()*price;
        }
    }

    public synchronized String getItemOrdered() {
        return itemOrdered;
    }

    public synchronized int getQuantityOrdered() {
        return quantityOrdered;
    }

    public synchronized String getCustomerName() {
        return customerName;
    }

    public synchronized void setItemOrdered(String itemOrdered) {
        this.itemOrdered = itemOrdered;
    }

    public synchronized void setQuantityOrdered(int quantityOrdered) {
        this.quantityOrdered = quantityOrdered;
    }

    public synchronized void setCustomerName(String customerName) {
        this.customerName = customerName;
    }
}

解决方案 2

创建一个不可变的对象(一个一旦创建就不会改变其内部状态的对象)。在下面的代码中,对象的内部变量被声明为 final,并在构造时被赋值。这样做可以保证对象是不可变的:

class ImmutableCustomerOrder {
    final private String itemOrdered;
    final private int quantityOrdered;
    final private String customerName;

    ImmutableCustomerOrder(String itemOrdered, int quantityOrdered, String customerName) {
        this.itemOrdered = itemOrdered;
        this.quantityOrdered = quantityOrdered;
        this.customerName = customerName;
    }

    public String getItemOrdered() {
        return itemOrdered;
    }

    public int getQuantityOrdered() {
        return quantityOrdered;
    }

    public String getCustomerName() {
        return customerName;
    }

    public double calculateOrderTotal (double price) {
        return getQuantityOrdered()*price;
    }
}

它是如何工作的

解决方案 1 依赖于锁保护对对象所做的任何更改的原则。使用 synchronized 关键字是编写 synchronized (this)表达式的捷径。通过同步 getters 和 setters(以及任何其他改变对象内部状态的操作),可以保证对象是一致的。同样重要的是,任何作为一个单元发生的操作(比如同时修改两个集合的操作,如配方 10-5 中所列)都在对象的方法中完成,并使用 synchronized 关键字保护。

例如,如果一个对象提供了 getSize()方法和 getItemNumber(int index),那么编写下面的 object . getItemNumber(object . getSize()-1)是不安全的。尽管语句看起来很简洁,但另一个线程可以在获取大小和获取项目编号之间改变对象的内容。相反,创建 object.getLastElement()方法更安全,该方法自动计算大小和最后一个元素。

解决方案 2 依赖于不可变对象的属性。不可变对象不能改变它们的内部状态,不能改变它们的内部状态的对象(是不可变的)根据定义是线程安全的。如果由于某个事件而需要修改不可变对象,不要显式地更改其属性,而是用更改后的属性创建一个新对象。然后,这个新对象将取代旧对象,在将来请求该对象时,将返回新的不可变对象。这是迄今为止创建线程安全代码最简单(尽管冗长)的方法。

10-9.实现线程安全计数器

问题

您需要一个线程安全的计数器,以便它可以在不同的执行线程中递增。

解决办法

通过使用固有的线程安全原子对象,可以创建一个保证线程安全并具有优化的同步策略的计数器。在以下代码中,创建了一个 Order 对象,它需要一个唯一的 order ID,该 ID 是使用 AtomicLong incrementAndGet()方法生成的:

AtomicLong orderIdGenerator = new AtomicLong(0);

        for (int i =0;i < 10;i++) {
            Thread orderCreationThread = new Thread(() -> {
                for (int i1 = 0; i1 < 10; i1++) {
                    createOrder(Thread.currentThread().getName());
                }
            });
            orderCreationThread.setName("Order Creation Thread "+i);
            orderCreationThread.start();
        }

//////////////////////////////////////////////////////
    private void createOrder(String name) {
        long orderId = orderIdGenerator.incrementAndGet();
        Order order = new Order(name, orderId);
        orders.add(order);
    }

它是如何工作的

AtomicLong(及其同类 AtomicInteger)是为在并发环境中安全使用而构建的。他们有方法自动递增(并获得)更改后的值。即使数百个线程调用 AtomicLong increment()方法,返回值也总是唯一的。

如果您需要做出决策和更新变量,请始终使用 AtomicLong 提供的原子操作;比如 compareAndSet。否则,你的代码就不是线程安全的(因为任何先检查后动作的操作都需要是原子的),除非你使用自己的锁从外部保护原子引用(见方法 10-7)。

下面的代码演示了几个需要注意的代码安全问题。首先,更改一个长整型值可以在两次内存写操作中完成(Java 内存模型允许),因此两个线程可能会在表面上看起来是线程安全的代码中重叠这两次操作。结果将是一个完全出乎意料的(很可能是错误的)长整型值:

long counter = 0;

public long incrementCounter() {
  return counter++;
}

这段代码还受到不安全发布的影响,不安全发布指的是变量可能被本地缓存(在 CPU 的内部缓存中)并且可能不会被提交到主内存中。如果另一个线程(在另一个 CPU 中执行)碰巧从主存中读取变量,那么那个线程可能会错过第一个线程所做的更改。改变的值可能被第一线程的 CPU 缓存,并且还没有被提交到第二线程可以看到它的主存储器。为了安全发布,必须使用 volatile Java 修饰符(见download . Oracle . com/javase/tutorial/essential/concurrency/atomic . html)。

前面代码的最后一个问题是它不是原子的。尽管看起来只有一个操作来递增计数器,但实际上有两个操作发生在机器语言级别(检索变量,然后递增)。可能有两个或更多的线程获得了相同的值,因为它们都检索了变量,但还没有递增它。然后所有线程将计数器递增到相同的数字。

10-10.将任务分解成离散的工作单元

问题

您有一个受益于使用分治策略的算法,分治策略指的是将一个工作单元分解成两个独立的子单元,然后将这些子单元的结果拼凑在一起的能力。然后子单元可以被分解成更多的工作子单元,直到工作小到足以被执行。通过将工作单元分解成子单元,您可以毫不费力地利用当今处理器的多核特性。

解决办法

新的 Fork/Join 框架使得分而治之策略的应用变得简单明了。以下示例创建了一个生活游戏的表示。代码使用 Fork/Join 框架来加速从一代到下一代的每次迭代的计算:

////////////////////////////////////////////////////////////////

        ForkJoinPool pool = new ForkJoinPool();
        long i = 0;

        while (shouldRun) {
            i++;
            final boolean[][] newBoard = new boolean[lifeBoard.length][lifeBoard[0].length];
            long startTime = System.nanoTime();
            GameOfLifeAdvancer advancer = new GameOfLifeAdvancer(lifeBoard, 0,0, lifeBoard.length-1, lifeBoard[0].length-1,newBoard);
            pool.invoke(advancer);
            long endTime = System.nanoTime();
            if (i % 100 == 0 ) {
                System.out.println("Taking "+(endTime-startTime)/1000 + "ms");
            }
            SwingUtilities.invokeAndWait(() -> {
                model.setBoard(newBoard);
                lifeTable.repaint();
            });
            lifeBoard = newBoard;
        }
////////////////////////////////////////////////////////////////

    class GameOfLifeAdvancer extends RecursiveAction{

        private boolean[][] originalBoard;
        private boolean[][] destinationBoard;
        private int startRow;
        private int endRow;
        private int endCol;
        private int startCol;

        GameOfLifeAdvancer(boolean[][] originalBoard, int startRow, int startCol, int endRow, int endCol, boolean [][] destinationBoard) {
            this.originalBoard = originalBoard;
            this.destinationBoard = destinationBoard;
            this.startRow = startRow;
            this.endRow = endRow;
            this.endCol = endCol;
            this.startCol = startCol;
        }

        private void computeDirectly() {
            for (int row = startRow; row <= endRow;row++) {
                for (int col = startCol; col <= endCol; col++) {
                    int numberOfNeighbors = getNumberOfNeighbors (row, col);
                    if (originalBoard[row][col]) {
                        destinationBoard[row][col] = true;
                        if (numberOfNeighbors < 2) destinationBoard[row][col] = false;
                        if (numberOfNeighbors > 3) destinationBoard[row][col] = false;
                    } else {
                        destinationBoard[row][col] = false;
                        if (numberOfNeighbors == 3) destinationBoard[row][col] = true;
                    }
                }
            }
        }

        private int getNumberOfNeighbors(int row, int col) {
            int neighborCount = 0;
            for (int leftIndex = -1; leftIndex < 2; leftIndex++) {
                for (int topIndex = -1; topIndex < 2; topIndex++) {
                    if ((leftIndex == 0) && (topIndex == 0)) continue; // skip own
                    int neighbourRowIndex = row + leftIndex;
                    int neighbourColIndex = col + topIndex;
                    if (neighbourRowIndex<0) neighbourRowIndex =
                    originalBoard.length + neighbourRowIndex;
                    if (neighbourColIndex<0) neighbourColIndex =
                    originalBoard[0].length + neighbourColIndex ;
                    boolean neighbour = originalBoard[neighbourRowIndex % originalBoard.length][neighbourColIndex % originalBoard[0].length];
                    if (neighbour) neighborCount++;
                }
            }
            return neighborCount;
        }

        @Override
        protected void compute() {
            if (getArea() < 20) {
                computeDirectly();
                return;
            }
            int halfRows = (endRow - startRow) / 2;
            int halfCols = (endCol - startCol) / 2;
            if (halfRows > halfCols) {
                // split the rows
                invokeAll(new GameOfLifeAdvancer(originalBoard, startRow, startCol, startRow+halfRows, endCol,destinationBoard),
                          new GameOfLifeAdvancer(originalBoard, startRow+halfRows+1, startCol, endRow, endCol,destinationBoard));
            } else {
                invokeAll(new GameOfLifeAdvancer(originalBoard, startRow, startCol, endRow, startCol+ halfCols,destinationBoard),
                          new GameOfLifeAdvancer(originalBoard, startRow, startCol+halfCols+1, endRow, endCol,destinationBoard));
            }
        }

        private int getArea() { return (endRow - startRow) * (endCol - startCol);  }

    }

它是如何工作的

Fork/Join 框架可用于将任务分解成离散的工作单元。解决方案的第一部分创建了一个 ForkJoinPool 对象。默认构造函数提供了合理的默认值(比如创建与 CPU 内核一样多的线程),并设置了一个入口点来提交各个击破的工作。虽然 ForkJoinPool 继承自 ExecutorService,但它最适合处理从 RecursiveAction 扩展而来的任务。ForkJoinPool 对象具有 invoke(RecursiveAction)方法,该方法将接受一个 RecursiveAction 对象并应用分治策略。

解决方案的第二部分创建 GameOfLifeAdvancer 类,该类扩展了 RecursiveAction 类。通过扩展 RecursiveAction 类,可以拆分工作。GameOfLifeAdvancer 类将人生板的游戏推进到了下一代。构造器取一个二维布尔数组(代表一个生命棋盘的游戏),一个起始行/列,一个结束行/列,一个目的二维布尔数组,在上面收集了推进生命棋盘游戏一代的结果。

GameOfLifeAdvancer 是实现 compute()方法所必需的。用这种方法,确定有多少工作要完成。如果工作足够小,则直接完成工作(通过调用 computed directly()方法并返回来实现)。如果工作不够小,该方法通过创建两个 gameoflifeadevancer 实例来拆分工作,这两个实例只处理当前 gameoflifeadevancer 工作的一半。这是通过将要处理的行数分成两个块或者将列数分成两个块来实现的。然后,通过调用 RecursiveAction 类的 invokeAll()方法,将两个 GameOfLifeAdvancer 实例传递给 ForkJoin 池。invokeAll()方法获取 GameOfLifeAdvancer 的两个实例(可以根据需要获取任意多个实例)并等待,直到它们都执行完毕(即 invokeAll()方法名称中–all 后缀的含义;它在返回控制之前等待提交的所有任务完成)。

通过这种方式,GameOfLifeAdvancer 实例被分解成新的 GameOfLifeAdvancer 实例,每个实例只处理人生棋盘游戏的一部分。每个实例在将控制权返回给调用者之前都要等待所有从属部分完成。由此产生的工作分工可以利用当今典型系统中可用的多个 CPU。

小费

ForkJoinPool 通常比 ExecutorService 更有效,因为它实现了工作窃取策略。每个线程都有一个工作队列要完成;如果任何线程的队列为空,该线程将从另一个线程队列中“窃取”工作,从而更有效地利用 CPU 处理能力。

10-11.跨多个线程更新公共值

问题

您的应用需要跨多个线程安全地维护一个求和值。

解决办法

利用 DoubleAdder 或 LongAdder 来包含在多个线程中求和的值,以确保安全处理。在下面的示例中,两个线程同时向 DoubleAdder 添加值,最后将值相加并显示出来。

DoubleAdder da = new DoubleAdder();

private void start() {

        Thread thread1 = new Thread(() -> {
            for (int i1 = 0; i1 < 10; i1++) {
                da.add(i1);
                System.out.println("Adding " + i1);
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i1 = 0; i1 < 10; i1++) {
                da.add(i1);
                System.out.println("Adding " + i1);
            }
        });

        thread1.start();
        thread2.start();

        try {
            System.out.println("Sleep while summing....");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("The sum is: " + da.doubleValue());

    }
}

结果:

Adding 0
Adding 1
Adding 2
Adding 3
Adding 4
Adding 5
Adding 6
Adding 7
Adding 0
Adding 8
Adding 9
Adding 1
Adding 2
Adding 3
Adding 4
Adding 5
Adding 6
Adding 7
Adding 8
Adding 9
The sum is: 90.0

它是如何工作的

在 Java 8 发布之前,在多线程中处理值时利用原子序数是很重要的。原子变量防止线程干扰,而不会像同步访问在某些情况下那样造成阻塞。Java 8 引入了一系列新的原子变量,提供了比标准原子变量更快的吞吐量。在大多数情况下,当可以跨多个线程访问和更新值时,Java . util . concurrent . atomic . double adder 和 Java . util . concurrent . atomic . long adder 类优于 AtomicDouble 和 AtomicLong。DoubleAdder 和 LongAdder 都扩展了数字,它们在对线程间的值求和时非常有用,尤其是在高度争用的情况下。

在该解决方案中,双加法器用于对两个不同线程的数字求和。使用 add()方法,各种数字被“加”到 DoubleAdder 值上。在线程有足够的时间执行它们的工作后,调用 doubleValue()方法以双精度形式返回所有值的总和。

DoubleAdder 和 LongAdder 类都包含类似的方法,尽管 LongAdder 包含两个额外的助手方法,用于递增和递减加法器的值。表 10-2 显示了每个类中包含的方法。

表 10-2。双加法器和长加法器方法
|

方法

|

描述

|
| --- | --- |
| 添加() | 添加给定值。 |
| 减量() | (仅限 LongAdder。)相当于 add(-1)。 |
| doubleValue() | 将 sum()作为双精度值返回(在对 LongAdder 执行扩大基元转换后)。 |
| floatValue() | 执行扩大基元转换后,将 sum()作为浮点值返回。 |
| 增量() | (仅限 LongAdder。)相当于 add(1)。 |
| intValue() | 执行收缩转换后,将 sum()作为 int 值返回。 |
| longValue() | 将 sum()作为长整型值返回(在 DoubleAdder 上执行收缩转换后)。 |
| 重置() | 将变量值重置为零。 |
| 总和() | 返回当前的合计值。 |
| sumThenReset() | 返回当前的合计值,然后将变量的值重置为零。 |
| toString() | 返回求和值的字符串表示形式。 |

小费

DoubleAccumulator 和 LongAccumulator 类与 DoubleAdder 和 LongAdder 属于同一家族。这些类允许使用提供的函数来更新跨线程维护的一个或多个变量。这两个类都接受一个累加器函数作为第一个参数,一个标识作为第二个参数。当跨线程应用更新时,用于执行计算的变量集可以动态增长以减少争用。有关这些 Java 8 新增类的更多信息,请参考在线文档:docs . Oracle . com/javase/9/docs/API/Java/util/concurrent/atomic/package-summary . html

10-12.异步执行多个任务

问题

您的应用需要以异步方式同时执行多个任务,这样任务之间就不会互相阻塞。

解决办法

利用 CompletableFuture 对象来表示当前正在执行的每个任务的状态。每个 CompletableFuture 对象将在指定的或应用确定的后台线程上运行,一旦完成,就向原始调用方法发出回调。

在下面的解决方案中,调用方法调用了两个长期运行的任务,一旦任务完成,它们都利用 CompletableFuture 来报告状态。

public class Recipe10_12 {

    public static void main(String[] args) {
        try {
            CompletableFuture tasks = performWork()
                    .thenApply(work -> {
                        String newTask = work + " Second task complete!";
                        System.out.println(newTask);
                        return newTask;
                    }).thenApply(finalTask -> finalTask + " Final Task Complete!");

            CompletableFuture future = performSecondWork("Java 9 is Great! ");
            while(!tasks.isDone()){
               System.out.println(future.get());
            }
            System.out.println(tasks.get());

        } catch (ExecutionException | InterruptedException ex) {

        }
    }

    /**
     * Returns a CompleableFuture object.
     * @return
     */
    public static CompletableFuture performWork() {
        CompletableFuture resultingWork = CompletableFuture.supplyAsync(
                () -> {
                    String taskMessage = "First task complete!";
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ex) {
                        System.out.println(ex);
                    }
                    System.out.println(taskMessage);
                    return taskMessage;
                });
        return resultingWork;

    }

    /**
     * Accepts a String and returns a CompletableFuture.
     * @param message
     * @return
     */
    public static CompletableFuture performSecondWork(String message) {
        CompletableFuture resultingWork = CompletableFuture.supplyAsync(
                () -> {
                    String taskMessage = message + " Another task complete!";
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ex) {
                        System.out.println(ex);
                    }

                    return taskMessage;
                });
        return resultingWork;

    }
}

结果:

First task complete!
First task complete! Second task complete!
Java 9 is Great! Another task complete!
First task complete! Second task complete! Final Task Complete!

它是如何工作的

Java 8 中添加了 CompletableFuture 来构建对异步任务的支持。CompletableFuture 是 Future 的扩展,它增加了许多方法来促进异步、事件驱动的编程模型,并且还允许随时设置值。后一种功能意味着 CompletableFuture 可以在需要之前创建,以防应用将来需要使用它。

创建 CompletableFuture 对象有两个选项,可以手动创建,也可以通过使用工厂方法来创建。手动创建 CompleteableFuture 遗嘱可以在不绑定到任何线程的情况下完成,这种策略在诸如应用需要未来将发生的事件的占位符的情况下会很有用。以下代码演示了如何手动创建 CompletableFuture:

final <CompletableFutureString> completableFuture = new CompletableFuture<>();

人们可以利用工厂生成 CompletableFuture 来返回一个面向特定任务或结果的对象。有许多不同的工厂方法可以调用来返回这样的对象。有些工厂方法接受参数,有些不接受。例如,CompletableFuture . run async(Runnable)方法返回 CompletableFuture,该方法首先执行提供的 Runnable,然后通过在 ForkJoinPool.commonPool()中运行的任务异步完成。runAsync()方法的另一个变体接受 Runnable 和 Executor,它首先执行提供的 Runnable,然后由给定 Executor 中的任务异步完成。

CompletableFuture 对象还包含许多与标准 Future 对象非常相似的方法。例如,isDone()、cancel()和 isCompletedExceptionally()方法都返回布尔值来指示对象的状态。通过调用接受 lambda 表达式和方法引用的 thenApply()方法,还可以用 CompletableFuture 来堆叠异步任务。这个配方的解决方案演示了如何利用 thenApply()方法从另一个调用异步任务。首先,执行名为 performWork()的 CompletableFuture 对象,然后执行 lambda,根据 performWork()中生成的字符串创建连接字符串。一旦第二个任务完成,就会调用另一个任务向字符串追加更多的文本。然后在一个循环中调用 future.get()方法,以便查看应用随时间推移而转换的字符串。最后,打印完全完成的任务的结果。

Java 9 为 CompletableFuture 增加了一些增强功能。通过维护一个触发和取消动作的线程,现在对延迟和超时有了更好的支持。它还保持了对子类化和一些实用方法的更好支持。

摘要

在开发应用时,理解并发的基本原理是很重要的。没有什么比成功地测试一个应用,然后在它发布到生产环境中时因为死锁而失败更糟糕的了。本章从基础开始,演示了如何产生一个后台任务。然后,本文介绍了处理并发性的各种技术,从创建线程到使用 Fork/Join 框架将工作分成离散的任务。最后,本章以 CompletableFuture 的覆盖范围和 Java 9 中该类的一些新增内容结束。

十一、调试和单元测试

调试是软件开发的一大部分。为了有效地进行调试,您必须能够像计算机一样“思考”并深入代码,解构导致您正在努力解决的逻辑错误的每一步。在计算机编程的初期,没有很多工具可以帮助调试。大多数情况下,调试包括查看您的代码并找出不一致的地方;然后重新提交代码再次编译。今天,每个 IDE 都提供了使用断点和检查内存变量的能力,这使得调试变得更加容易。在 ide 之外,还有其他工具可以帮助您进行项目的日常调试、构建和测试;这些工具确保您的代码不断地被测试,以发现编程时可能引入的错误。在这一章中,您将探索有助于调试、分析和测试 Java 软件的不同工具。

本章涵盖了一些调试和单元测试的基础知识。您将学习如何使用 Apache Ant 和 JUnit 从命令行或终端执行单元测试。您还将了解如何利用 NetBeans Profiler 以及其他工具来分析和监视您的应用。

11-1.了解异常

问题

您捕获并记录了一个异常,您需要确定其原因。

解决办法

分析异常的 printStackTrace()方法的输出:

public class Recipe11_1 {
    public static void main (String[] args) {
        Recipe11_1 recipe = new Recipe11_1();
        recipe.startProcess();
    }

    private void startProcess() {
        try {
            int a = 5/0;
        }  catch (Exception e) {
            e.printStackTrace();
        }

    }
}

结果:

java.lang.ArithmeticException: / by zero
   at org.java8recipes.chapter11.recipe11_01.Recipe11_1.start(Recipe11_1.java:18)
   at org.java8recipes.chapter11.recipe11_01.Recipe11_1.main(Recipe11_1.java:13)

它是如何工作的

在编程行话中,指的是被调用以到达程序中某一点的函数列表,通常从即时函数(System.out.println())开始到更一般的函数(public static void main)。每个程序都跟踪哪个代码被执行,以便到达代码的特定部分。栈跟踪的输出是指发生错误时内存中的栈。Java 中抛出的异常跟踪它们发生的位置,以及抛出异常时执行的代码路径。栈跟踪显示从发生异常的最具体的地方(发生异常的那一行)到出错代码的顶级调用程序(以及中间的所有内容)。然后,这些信息允许您查明执行了哪些方法调用,并可能有助于了解引发异常的原因。

在本例中,被零除异常发生在 Recipe11_1.java 的第 18 行,是由 main()方法的调用引起的(在第 13 行)。有时,当查看栈跟踪的输出时,您会看到不属于项目的方法。这是自然发生的,因为有时方法调用是在工作系统的其他部分生成的。例如,当出现异常时,在 Swing 应用中看到抽象窗口工具包(AWT)方法是很常见的(由于 EventQueue 的性质)。如果您查看更具体的函数调用(最早),您最终将使用项目自己的代码运行,然后可以尝试确定引发异常的原因。

注意

如果用“调试”信息编译程序,栈跟踪输出将包含行号信息。默认情况下,大多数 ide 在调试配置中运行时都会包含这些信息。通常,IDE 还会生成一个直接链接,将您带到有问题的代码行,从而使错误的行号变得易于访问。如果使用命令行,请使用–g 选项来编译和生成调试信息。

11-2.锁定类的行为

问题

您需要锁定您的类的行为,并希望创建将用于验证您的应用中的特定行为的单元测试。

解决办法

使用 JUnit 创建单元测试来验证类中的行为。要使用这个解决方案,您需要在您的类路径中包含 JUnit 依赖项。JUnit 可以从www.junit.org下载,或者你可以简单地将 Maven 依赖项添加到你的项目中。如果您选择下载它,那么您将需要加载 junit.jar 和 hamcrest.jar。在撰写本文时,Maven 依赖关系如下,请相应地更改版本:

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>

当 JUnit 成为项目的一部分时,您将能够包含 org.junit 和 junit.framework 名称空间。在这个例子中,为 MathAdder 类创建了两个单元测试。MathAdder 类包含两个方法:addNumber (int,int)和 subtract number(int,int)。这两个方法返回它们传递的参数的加法(或减法)(一个简单的类)。单元测试(由@Test 注释标记)验证 MathAdder 类实际上是将两个数相加和/或相减。

package org.java8recipes.chapter11;

import junit.framework.Assert;
import org.junit.Test;

public class Recipe11_2_MathAdderTest {

    @Test
    public void testAddBehavior() {
        Recipe_11_2_MathAdder adder = new Recipe_11_2_MathAdder();
        for (int i =0;i < 100;i++) {
            for (int j =0;j < 100;j++) {
                Assert.assertEquals(i+j,adder.addNumbers(i,j));

            }
        }
    }

    @Test
    public void testSubstractBehavior() {
        Recipe_11_2_MathAdder adder = new Recipe_11_2_MathAdder();
        for (int i =0;i < 100;i++) {
            for (int j =0;j < 100;j++) {
                Assert.assertEquals(i-j,adder.substractNumber(i,j));

            }
        }
    }
}

要执行此测试,请使用您的 IDE 来运行测试类。例如,在 NetBeans 中,您必须通过右键单击 test 类并将其移动到 NetBeans 项目中的“Test Packages”模块来重构它。一旦您将测试类移动到“Test Packages”中所需的包中,右键单击并运行文件来执行测试。

注意

在撰写本文时,JUnit 5 库正在积极开发中。它是 JUnit 的下一代,包括许多新的功能,利用了较新的 JVM 语言结构,比如 lambdas。这个方法主要关注 JUnit 4,因为它是一个成熟的测试套件。有关 JUnit 5 的更多信息,请参考以下网站:junit.org/junit5/

它是如何工作的

单元测试对于测试你的代码以确保预期的行为发生在你的类中是有用的。在项目中包含单元测试可以减少添加或重构代码时破坏功能的可能性。当您创建单元测试时,您正在指定一个对象应该如何行为(这被称为它的契约)。单元测试确保预期的行为发生(他们通过验证方法的结果和使用不同的 JUnit 来做到这一点。断言方法)。

编写单元测试的第一步是创建一个新的类,描述您想要验证的行为。一个通用的单元测试命名约定是创建一个与被测试类同名的类,后缀为 Test;在这个菜谱的例子中,主类称为 Recipe11_2_MathAdder,而测试类称为 Recipe11_2_MathAdderTest。

单元测试类(MathAdderTest)将包含检查和验证类行为的方法。为此,对方法名进行了注释。注释是元数据的形式,开发者可以“注释”代码的指定部分,从而将信息添加到注释的代码中。程序不使用这些额外的信息,而是由编译器/构建器(或外部工具)来指导代码的编译、构建和/或测试。出于单元测试的目的,您可以通过在每个方法名之前指定 @Test 来注释作为单元测试一部分的方法。在每个方法中,使用 Assert.assertEquals(或任何其他 Assert 静态方法)来验证行为。

Assert.assertEquals 方法指示单元测试框架验证您正在测试的类的方法调用的预期值与其方法调用返回的实际值是否相同。在配方示例中,Assert.assertEquals 验证 MathAdder 是否正确地将两个整数相加。虽然这个类的范围很小,但是它显示了进行全功能单元测试的最低要求。

如果断言调用成功,它在单元测试框架中被报告为“通过”测试;如果 Assert 调用失败,那么单元测试框架将停止并显示一条消息,显示单元测试失败的地方。大多数现代的 ide 都有运行单元测试类的能力,只需右击名字并选择 Run/Debug(这是运行 Chapter_11_2_MathAdderTest 方法的预期方式)。

诚然,ide 可以在开发的同时运行单元测试,但它们是为了自动运行而创建的(通常由预定的构建或版本控制系统的签入来触发),这就是方法 11-3 所说的。

11-3.编写单元测试脚本

问题

您希望自动运行单元测试,而不是手动调用它们。

解决办法

使用和配置 JUnit 和 Ant。为此,请按照下列步骤操作:

  1. 下载 Apache Ant(位于ant.apache.org/)。

  2. 将 Apache Ant 解压缩到一个文件夹中(例如,对于 Windows 系统为 c:\ant,对于 OS X 系统为/Development)。

  3. 确保 Apache Ant 可以从命令行或终端执行。在 Windows 中,这意味着将 apache-ant/bin 文件夹添加到路径,如下所示:

    1. 转到控制面板➤系统。

    2. 单击高级系统设置。

    3. 单击环境变量。

    4. 在系统变量列表中,双击变量名路径。

    5. 在字符串的末尾,添加;C:\apache-ant-1.8.2\bin(或您解压缩 Apache Ant 的文件夹)。

    6. 单击 OK(在之前打开的每个弹出框上)接受更改。

    注意 Apache Ant 预装在 OS X 上,因此您不必安装或配置它。要验证这一点,请打开终端窗口并键入 ant–version,以查看系统上安装的是哪个版本。

    确保定义了 JAVA_HOME 环境变量。在 Windows 中,这意味着添加一个名为 JAVA_HOME 的新环境变量。例如:

    转到控制面板➤系统。

  4. 单击高级系统设置。

  5. 单击环境变量。在系统变量列表中,检查是否有名为 JAVA_HOME 的变量,以及该值是否指向您的 JDK 发行版。如果 JAVA_HOME 不存在,请单击新建。将变量名设置为 JAVA_HOME,将变量值设置为 C:\Program Files\Java\jdk1.9.0 或 JDK 9 安装的根目录。

    在 OS X 上,环境变量是在。bash 概要文件,驻留在用户主目录中。要添加 JAVA_HOME,请在。bash_profile:

    export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.9.0.jdk/Contents/Home
    

    测试您是否可以联系到 Ant,并且 Ant 可以找到您的 JDK 安装。要测试更改是否生效,请执行以下操作:

  6. 打开命令窗口或终端。

  7. 类型 ant

如果您收到消息“Ant 未被识别为内部或外部命令”,请重复设置 PATH 变量的第一步(第一组指令)。如果您收到消息“无法定位 tools.jar”,您需要为您的安装创建和/或更新 JAVA_HOME 路径(第二组指令)。

消息“Buildfile: build.xml 不存在!”意味着您的设置已经可以使用 Ant 构建了。恭喜你!

注意

在 Microsoft Windows 或 OS X 中更改环境变量时,必须关闭以前的命令行或终端窗口,然后重新打开它们,因为更改只应用于新的命令窗口。

在项目的根目录下创建 build.xml,并将下面的基本 Ant 脚本作为 build.xml 文件的内容。这个特定的 build.xml 文件包含 Ant 将用来编译和测试这个菜谱的信息。

<project default="test" name="Chapter11Project" basedir=".">
<property name="src" location="src"/>
<property name="build" location="build/"/>
<property name="src.tests" location="src/"/>
<property name="reports.tests" location="report/" />

<path id="build.path">
<fileset dir="dep">
<include name="**/*.jar" />
</fileset>
<pathelement path="build" />
</path>

<target name="build">
<mkdir dir="${build}" />
<javac srcdir="${src}" destdir="${build}">
<classpath refid="build.path" />
</javac>
</target>

<target name="test" depends="build">
<mkdir dir="${reports.tests}" />
<junit fork="yes" printsummary="yes" haltonfailure="yes">
<classpath refid="build.path" />
<formatter type="plain"/>

<batchtest fork="yes" todir="${reports.tests}">
<fileset dir="${src.tests}">
<include name="**/*Test*.java"/>
</fileset>
</batchtest>
</junit>
</target>
</project> 
注意

要执行此配方,请打开命令行窗口或终端,导航到 Chapter 11 文件夹,键入 ant,然后按 Enter 键。

它是如何工作的

Apache Ant(或简称 Ant)是一个允许您编写项目构建和单元测试脚本的程序。通过配置 Ant,您可以使用命令行构建、测试和部署您的应用。(反过来,它可以被安排由操作系统自动运行。)Ant 可以自动运行单元测试并报告这些测试的结果。这些结果可以在每次运行后进行分析,以查明行为的变化。

由于 Ant 的复杂性,它有一个很大的学习曲线,但它允许在编译、构建和编织代码方面有很大的灵活性。通过使用 Ant,有可能在如何构建项目上实现最大限度的配置。

注意

访问ant.apache.org/manual/index.html获得更深入的 Ant 教程。

build.xml 文件包含有关如何编译项目、使用哪个类路径以及运行哪些单元测试的说明。每个 build.xml 包含一个标签,它封装了构建项目的步骤。在每个中有目标,它们是构建过程中的“步骤”。一个可以依赖于其他目标,允许您在项目中建立依赖关系(在这个菜谱的例子中,目标“test”依赖于目标“build”,这意味着要运行测试目标,Ant 将首先运行构建目标)。

每个目标都包含任务。这些任务是可扩展的,并且有一组核心任务可以开箱即用。任务将编译 src 属性中指定的一组 Java 文件,并将输出写入 dest 属性。作为任务的一部分,您可以指定使用哪个类路径。在本例中,类路径是通过引用先前定义的路径(称为 build.path)来指定的。在这个配方中,类路径被定义为任何具有。jar 扩展位于 dep 文件夹中。

构建目标中的另一个任务是。此任务将找到在其任务中指定的单元测试并运行它。单元测试在属性中定义。通过使用属性,可以告诉 JUnit 查找名称中包含单词 Test 并以。java 扩展。一旦 JUnit 运行了每个测试,它将向控制台写出一个摘要,并向 reports.tests 文件夹写入一个关于单元测试结果的报告。

注意

您可以使用标签在 build.xml 文件中定义变量。定义属性后,可以使用${propertyName}语法将其作为另一个任务的一部分进行访问。这允许您快速更改构建脚本以响应结构变化(例如,切换目标/源文件夹)。

11-4.尽早发现漏洞

问题

您希望确保能够在设计时找到最大数量的 bug。

解决办法

使用 FindBugs 来扫描您的软件的问题。使用包含 FindBugs 的 Ant 构建文件进行报告。

以下是添加 FindBugs 报告的新 build.xml 文件:

<project default="test" name="Chapter11Project" basedir=".">

<property name="src" location="src"/>
<property name="build" location="build/"/>
<property name="reports.tests" location="report/" />
<property name="classpath" location="dep/" />

<!-- Findbugs Static Analyzer Info -->
<property name="findbugs.dir" value="dep/findbugs" />
<property name="findbugs.report" value="findbugs" />

<path id="findbugs.lib" >
<fileset dir="${findbugs.dir}" includes="*.jar"/>
</path>
<taskdef name="findbugs" classpathref="findbugs.lib" classname="edu.umd.cs.findbugs.anttask.FindBugsTask"/>

<path id="build.path">
<fileset dir="dep">
<include name="**/*.jar" />
</fileset>
</path>

<target name="clean">
<delete dir="${build}" />
<delete dir="${reports.tests}" />
<delete dir="${coverage.dir}" />
<delete dir="${instrumented}" />
<mkdir dir="${build}" />
<mkdir dir="${reports.tests}" />
<mkdir dir="${coverage.dir}" />

</target>

<target name="build">
<javac srcdir="${src}" destdir="${build}" debug="${debug}">
<classpath refid="build.path" />
</javac>
</target>

<target name="test" depends="clean,build">
<junit fork="yes" printsummary="yes" haltonfailure="yes">
<classpath refid="build.path" />
<formatter type="plain"/>

<batchtest fork="yes" todir="${reports.tests}">
<fileset dir="${build}">
<include name="**/*Test*.class"/>
</fileset>
</batchtest>
<jvmarg value="-XX:-UseSplitVerifier" />
</junit>

</target>

<target name="findbugs" depends="clean">
<antcall target="build">
<param name="debug" value="true" />
</antcall>

<mkdir dir="${findbugs.report}" />
<findbugs home="${findbugs.dir}"
                output="html"
                outputFile="${findbugs.report}/index.html"
                reportLevel="low"
>
<class location="${build}/" />
<auxClasspath refid="build.path" />
<sourcePath path="${src}" />
</findbugs>
</target>
</project>

要运行这个食谱,下载 FindBugs(findbugs.sourceforge.net/downloads.html)。解压缩到您计算机上的一个文件夹中,然后复制。/lib/ folder 到项目的/dep/findbugs 文件夹中(如果需要,创建/dep/findbugs 文件夹)。确保/dep/findbugs/findbugs.jar 和/dep/findbugs/findbugs-ant.jar 存在。

它是如何工作的

FindBugs 是一个静态代码分析器(SCA) 。它将分析你的程序的编译文件,并找出编码中常见的错误(不是语法错误,而是某些类型的逻辑错误)。例如,FindBugs 将发现的一个错误是使用==而不是 String.equals()比较两个字符串。然后将分析写成 HTML(或文本),可以用浏览器查看。从 FindBugs 中捕捉错误很容易,将它作为持续集成过程的一部分是非常有益的。

在 build.xml 的开头,您定义了 FindBugs 任务。本节指定。jar 文件定义了新任务(dep\findbugs ),并决定了完成后将报告放在哪里。

build.xml 还有一个名为“findbugs”的新目标项目。findbugs 目标编译包含调试信息的源文件(包含调试信息有助于 FindBugs 报告,因为它会在报告错误时包含行号),然后继续分析错误的字节码。在 findbugs 任务中,指定编译的。类文件(这是属性)、项目依赖项的位置(属性),以及源代码的位置(属性)。

在 findbugs 目标中,有一个任务。任务只是运行在任务中指定的目标。就在任务之前,您将调试赋值为 true。这又以 debug="${debug} "的形式传递给任务。当 debug 设置为 true 时,任务将把调试信息包含到 Java 源文件的编译中。在编译后的文件中包含调试信息将有助于生成可读性更好的 FindBugs 报告,因为它将包含问题所在的行号。在整个 build.xml 文件中使用从 Ant 目标中分配属性的技巧,以便在遍历特定的构建目标时有选择地启用某些行为。如果您要构建常规的构建目标,构建的结果将不包含调试信息。相反,如果您要构建 findbugs 目标,因为 findbugs 目标将 debug 替换为 true,那么构建的结果将包含调试信息。

小费

要调用 ant 来运行默认的“目标”(如 build.xml 中所指定的),只需键入 Ant。指定另一个。xml 文件(而不是 build.xml),键入 ant–f name of other file . XML。要更改要运行的默认目标,请在末尾键入目标的名称(例如, ant clean )。要运行此示例,请键入 ant–f FindBugs build . XML FindBugs。这将要求 Ant 使用 findbugsbuild.xml 文件并运行 findbugs 目标。

11-5.监控应用中的垃圾收集

问题

您注意到您的应用似乎变慢了,并怀疑正在进行垃圾收集。

解决方案 1

启动 Java 程序时,将-Xloggc:GC . log-XX:+PrintGCDetails-XX:+printgcstimestamps 作为参数。这些参数允许您将垃圾收集信息记录到 gc.log 文件中,包括垃圾收集发生的时间以及详细信息(是次要的还是主要的垃圾收集,以及花费了多长时间)。

Ant target that executes Recipe 11_5 with garbage logging on.

<target name="Recipe11_5" depends="build">
<java classname="org.java9recipes.chapter11.Recipe11_5" fork="true">
   <classpath refid="build.path" />
   <jvmarg value="-Xloggc:gc.log" />
   <jvmarg value="-XX:+PrintGCDetails" />
   <jvmarg value="-XX:+PrintGCTimeStamps" />
</java>
</target>

在这个 build.xml 文件中,Java 任务用于在启动应用之前向编译器添加垃圾收集日志记录的参数。要在整个 Ant 中运行这个示例,请键入 ant Recipe11_5

解决方案 2

使用 NetBeans“Profiler”工具分析程序的内存消耗等。要运行 profiler,请选择要对其执行性能分析的文件或项目,然后从 NetBeans“性能分析”菜单中选择“性能分析项目”或“性能分析文件”命令。您也可以用鼠标右键单击项目或文件,以访问上下文菜单配置文件选项。

Profiler 对话框(图 11-1 )将会打开,允许您选择和配置选项。在这个解决方案中,只需选择 Run 按钮,用默认设置执行概要分析。

A323910_3_En_11_Fig1_HTML.jpg

图 11-1。NetBeans 探查器

一旦 profiler 开始运行,它将一直运行,直到您使用“控制”面板上的“停止”按钮将其停止。生成的输出应该如图 11-2 所示。

A323910_3_En_11_Fig2_HTML.jpg

图 11-2。NetBeans 探查器结果

它是如何工作的

在解决方案 1 中为日志垃圾收集添加标志将导致您的 Java 应用将次要和主要垃圾收集信息写入一个日志文件。这允许您及时“重建”应用发生的情况,并发现可能的内存泄漏(或者至少是其他与内存相关的问题)。这是生产系统的首选故障排除方法,因为它通常是轻量级的,可以在垃圾收集发生后进行分析。

相反,解决方案 2 涉及到使用 NetBeans IDE 附带的开源工具。该工具允许您在代码运行时对其进行分析。这是一个很好的工具,可以原位了解您的应用中发生了什么,因为您可以看到实时 CPU 消耗、垃圾收集、创建的线程和加载的类。

这个方法仅仅触及了 NetBeans Profiler 的皮毛。更多信息,请参见位于profiler.netbeans.org/的在线文档。

注意

在使用 NetBeans Profiler 之前,必须校准目标 JVM。为此,请在 NetBeans 中打开“管理校准数据”对话框,并选择要校准的 JVM。通过打开配置文件菜单,然后选择高级命令,可以找到管理校准数据选项。

11-6.获取线程转储

问题

您的程序似乎什么也没做就“挂起”了,您怀疑可能出现了死锁。

解决办法

使用 JStack 获取线程转储,然后分析线程转储中的死锁。下面的 JStack 是来自 org . Java 9 recipes . chapter 11 . recipe 11 _ 06 类的线程转储。配方 11_6,它创建了一个死锁。Recipe11_6.java 的代码如下:

public class Recipe11_6 {
    Lock firstLock = new ReentrantLock();
    Lock secondLock = new ReentrantLock();

    public static void main (String[] args) {
        Recipe11_6 recipe = new Recipe11_6();
        recipe.start();
    }

    private void start() {
        firstLock.lock();
        Thread secondThread = new Thread(() -> {
            secondLock.lock();
            firstLock.lock();
        });

        secondThread.start();
        try {
            Thread.sleep(250);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        secondLock.lock();

        secondLock.unlock();
        firstLock.unlock();

    }

}

从命令行或 IDE 执行代码,然后使用操作系统实用工具(如任务管理器)检查进程 ID。从下面的命令中可以看出,示例代码正在进程 ID 为 19705 的情况下运行:

**jstack -l 19705** 

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.66-b17 mixed mode):

"Attach Listener" #11 daemon prio=9 os_prio=31 tid=0x00007f95c5818000 nid=0x380b waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

"Thread-0" #10 prio=5 os_prio=31 tid=0x00007f95c41ba000 nid=0x5503 waiting on condition [0x000000012afba000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076ab76698> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
        at org.java9recipes.chapter11.recipe11_06.Recipe11_6.lambda$start$0(Recipe11_6.java:25)
        at org.java9recipes.chapter11.recipe11_06.Recipe11_6$$Lambda$1/1418481495.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
        - <0x000000076ab766c8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

"Service Thread" #9 daemon prio=9 os_prio=31 tid=0x00007f95c4051000 nid=0x5103 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

"C1 CompilerThread3" #8 daemon prio=9 os_prio=31 tid=0x00007f95c4031800 nid=0x4f03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

"C2 CompilerThread2" #7 daemon prio=9 os_prio=31 tid=0x00007f95c4031000 nid=0x4d03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

"C2 CompilerThread1" #6 daemon prio=9 os_prio=31 tid=0x00007f95c4030000 nid=0x4b03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

"C2 CompilerThread0" #5 daemon prio=9 os_prio=31 tid=0x00007f95c402e800 nid=0x4903 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007f95c401a000 nid=0x3c17 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007f95c283a800 nid=0x3503 in Object.wait() [0x0000000128e91000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076ab070b8> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x000000076ab070b8> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

   Locked ownable synchronizers:
        - None

"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007f95c4003800 nid=0x3303 in Object.wait() [0x0000000128d8e000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076ab06af8> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157)
        - locked <0x000000076ab06af8> (a java.lang.ref.Reference$Lock)

   Locked ownable synchronizers:
        - None

"main" #1 prio=5 os_prio=31 tid=0x00007f95c280d800 nid=0x1303 waiting on condition [0x000000010d286000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076ab766c8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
        at org.java9recipes.chapter11.recipe11_06.Recipe11_6.start(Recipe11_6.java:34)
        at org.java9recipes.chapter11.recipe11_06.Recipe11_6.main(Recipe11_6.java:18)

   Locked ownable synchronizers:
        - <0x000000076ab76698> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

"VM Thread" os_prio=31 tid=0x00007f95c3830800 nid=0x3103 runnable

"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007f95c3005000 nid=0x2103 runnable

"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007f95c3005800 nid=0x2303 runnable

"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007f95c3006000 nid=0x2503 runnable

"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007f95c4000000 nid=0x2703 runnable

"GC task thread#4 (ParallelGC)" os_prio=31 tid=0x00007f95c4001000 nid=0x2903 runnable

"GC task thread#5 (ParallelGC)" os_prio=31 tid=0x00007f95c3007000 nid=0x2b03 runnable

"GC task thread#6 (ParallelGC)" os_prio=31 tid=0x00007f95c3007800 nid=0x2d03 runnable

"GC task thread#7 (ParallelGC)" os_prio=31 tid=0x00007f95c3807000 nid=0x2f03 runnable

"VM Periodic Task Thread" os_prio=31 tid=0x00007f95c401b000 nid=0x5303 waiting on condition

JNI global references: 308

Found one Java-level deadlock:
=============================
"Thread-0":
  waiting for ownable synchronizer 0x000000076ab76698, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "main"
"main":
  waiting for ownable synchronizer 0x000000076ab766c8, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "Thread-0"

Java stack information for the preceding threads:
===================================================
"Thread-0":
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076ab76698> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
        at org.java9recipes.chapter11.recipe11_06.Recipe11_6.lambda$start$0(Recipe11_6.java:25)
        at org.java9recipes.chapter11.recipe11_06.Recipe11_6$$Lambda$1/1418481495.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
"main":
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076ab766c8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
        at org.java9recipes.chapter11.recipe11_06.Recipe11_6.start(Recipe11_6.java:34)
        at org.java9recipes.chapter11.recipe11_06.Recipe11_6.main(Recipe11_6.java:18)

Found 1 deadlock.

要使这个方法在 Windows 上正常运行,您必须将 JDK 的 bin 文件夹作为 PATH 环境变量的一部分(例如 C:\ Program Files \ Java \ JDK 1 . 9 . 0 \ bin)。如果有这个路径,可以运行 JStack、JPS 之类的工具。JStack 预装在 OS X 上,所以你应该可以开箱即用。

JStack 命令使用–L(破折号和字母 L)作为参数,它指定了一个很长的列表(它做了额外的工作来获得关于正在运行的线程的更多信息)。JStack 还需要知道目标虚拟机的 PID。列出所有正在运行的 JVM 的一个快速方法是键入 JPS 并按 Enter 键。这将列出正在运行的虚拟机及其 PID。图 11-3 显示了一个 JStack 在 OS X 机器上发现配方 11-6 死锁的截图。

A323910_3_En_11_Fig3_HTML.jpg

图 11-3。JStack 结果
注意

在这个例子中,j.u.c.l 代表 java.util.concurrent.locks,aqs 代表 AbstractQueuedSynchronizer。

它是如何工作的

JStack 允许您查看当前正在运行的线程的所有栈跟踪。JStack 还会试图找到可能会让系统停滞的死锁(锁的循环依赖)。JStack 不会发现其他问题,例如活锁(当线程总是在旋转时,比如 while(true))或饥饿(当线程因为优先级太低或有太多线程争用资源而无法执行时),但它会帮助您了解程序中的每个线程正在做什么。

死锁的发生是因为一个线程正在等待另一个线程拥有的资源,而第二个线程正在等待第一个线程拥有的资源。在这种情况下,两个线程都无法继续,因为两个线程都在等待对方释放各自拥有的资源。死锁不仅发生在两个线程之间,还可能涉及一串线程,因此线程 A 正在等待线程 B 正在等待线程 C 正在等待线程 D 正在等待原始线程 A。了解转储以找到罪魁祸首资源非常重要。

在这个配方的示例中,Thread-0 想要获取名为 0x000000076ab76698 的锁;它在线程转储中被描述为“等待可拥有的同步器”Thread-0 无法获取锁,因为它由主线程持有。另一方面,主线程想要获取锁 0x000000076ab766c8(注意它们是不同的;第一个锁以 98 结尾,而第二个锁以 c8 结尾,由 Thread-0 持有。这是一个教科书式的死锁定义,每个线程都在等待对方释放另一个线程拥有的锁。

除了死锁之外,查看线程转储可以让您了解您的程序实时在做什么。特别是在多线程系统中,使用线程转储将有助于澄清线程在哪里休眠,或者它在等待什么条件。

注意

JStack 通常是轻量级的,足以在实时系统中运行,所以如果您需要对实时问题进行故障排除,您可以安全地使用 JStack。

摘要

在这一章中,我们看了一些最容易被忽视,但却是最重要的软件开发部分。为了确保交付可靠的软件,调试、单元测试和应用性能评估是必须执行的关键任务。有许多有用的实用程序可以完成这些任务,本章简要介绍了其中的一些。

十二、Unicode、国际化和货币代码

Java 平台提供了一组丰富的国际化特性来帮助您创建可以在世界范围内使用的应用。该平台提供了本地化您的应用的方法,以各种文化上适当的格式格式化日期和数字,并显示在许多书写系统中使用的字符。

本章只描述了程序员在开发国际化应用时必须执行的一些最常见的任务。因为 Java 语言在语言和区域的抽象方面增加了新的特性,所以本章描述了一些使用 Locale 类的新方法。其他新功能对开发人员来说是透明的,例如更新以符合较新的 Unicode 标准,但更新提供了合规性,因此 JDK 9 将在未来几年保持相关性。Java 9 支持 Unicode 7.0,增加了 3000 个字符和 20 多个脚本。

注意

本章示例的源代码可以在 org.java9recipes.chapter12 包中找到。

12-1.将 Unicode 字符转换为数字

问题

您希望将 Unicode 数字字符转换为其各自的整数值。例如,您有一个包含值 8 的泰国数字的字符串,并且您想用该值生成一个整数。

解决办法

java.lang.Character 类有几个静态方法将字符转换为整数数值:

  • 公共静态 intdigit(char ch,int radix)

  • 公共静态整数(整数,整数基数)

下面的代码片段遍历从 0x0000 到 0x10FFFF 的整个 Unicode 码位范围。对于每个也是数字的码位,它显示字符及其数字值 0 到 9。你可以在 org . Java 9 recipes . chapter 12 . recipe 12 _ 1 中找到这个例子。Recipe12_1 类。

int x = 0;
for (int c=0; c <= 0x10FFFF; c++) {
    if (Character.isDigit(c)) {
        ++x;
        System.out.printf("Codepoint: 0x%04X\tCharacter: %c\tDigit: %d\tName: %s\n", c, c,
            Character.digit(c, 10), Character.getName(c));            
    }
}
System.out.printf("Total digits: %d\n", x);

一些输出如下:

Codepoint: 0x0030    Character: 0    Digit: 0    Name: DIGIT ZERO
Codepoint: 0x0031    Character: 1    Digit: 1    Name: DIGIT ONE
Codepoint: 0x0032    Character: 2    Digit: 2    Name: DIGIT TWO
Codepoint: 0x0033    Character: 3    Digit: 3    Name: DIGIT THREE
Codepoint: 0x0034    Character: 4    Digit: 4    Name: DIGIT FOUR
Codepoint: 0x0035    Character: 5    Digit: 5    Name: DIGIT FIVE
Codepoint: 0x0036    Character: 6    Digit: 6    Name: DIGIT SIX
Codepoint: 0x0037    Character: 7    Digit: 7    Name: DIGIT SEVEN
Codepoint: 0x0038    Character: 8    Digit: 8    Name: DIGIT EIGHT
Codepoint: 0x0039    Character: 9    Digit: 9    Name: DIGIT NINE
Codepoint: 0x0660    Character: ٠    Digit: 0    Name: ARABIC-INDIC DIGIT ZERO
Codepoint: 0x0661    Character: ١    Digit: 1    Name: ARABIC-INDIC DIGIT ONE
Codepoint: 0x0662    Character: ٢    Digit: 2    Name: ARABIC-INDIC DIGIT TWO
Codepoint: 0x0663    Character: ٣    Digit: 3    Name: ARABIC-INDIC DIGIT THREE
Codepoint: 0x0664    Character: ٤    Digit: 4    Name: ARABIC-INDIC DIGIT FOUR
Codepoint: 0x0665    Character: ٥    Digit: 5    Name: ARABIC-INDIC DIGIT FIVE
Codepoint: 0x0666    Character: ٦    Digit: 6    Name: ARABIC-INDIC DIGIT SIX
Codepoint: 0x0667    Character: ٧    Digit: 7    Name: ARABIC-INDIC DIGIT SEVEN
Codepoint: 0x0668    Character: ٨    Digit: 8    Name: ARABIC-INDIC DIGIT EIGHT
Codepoint: 0x0669    Character: ٩    Digit: 9    Name: ARABIC-INDIC DIGIT NINE
...
Codepoint: 0x0E50    Character: ๐    Digit: 0    Name: THAI DIGIT ZERO
Codepoint: 0x0E51    Character: ๑    Digit: 1    Name: THAI DIGIT ONE
Codepoint: 0x0E52    Character: ๒    Digit: 2    Name: THAI DIGIT TWO
Codepoint: 0x0E53    Character: ๓      Digit: 3    Name: THAI DIGIT THREE
Codepoint: 0x0E54    Character: ๔    Digit: 4    Name: THAI DIGIT FOUR
Codepoint: 0x0E55    Character: ๕    Digit: 5    Name: THAI DIGIT FIVE
Codepoint: 0x0E56    Character: ๖    Digit: 6    Name: THAI DIGIT SIX
Codepoint: 0x0E57    Character: ๗     Digit: 7    Name: THAI DIGIT SEVEN
Codepoint: 0x0E58    Character: ๘     Digit: 8    Name: THAI DIGIT EIGHT
Codepoint: 0x0E59    Character: ๙     Digit: 9    Name: THAI DIGIT NINE
...
注意

示例代码打印到控制台。由于字体或平台的差异,您的控制台可能不会打印此示例中显示的所有字符标志符号。但是,这些字符将被正确地转换为整数。

它是如何工作的

Unicode 字符集很大,包含一百多万个唯一的码位,其整数值范围从 0x0000 到 0x10FFFF。每个字符值都有一组属性。其中一个属性是 isDigit。如果该属性为 true,则该字符表示从 0 到 9 的数字。例如,代码点值为 0x30 到 0x39 的字符具有字符标志符号 0、1、2、3、4、5、6、7、8 和 9。如果您简单地将这些代码值转换成它们对应的整数值,您将得到从 0x30 到 0x39 的十六进制值。对应的十进制值是 48 到 57。但是,这些字符也表示数字。当在计算中使用它们时,这些字符代表从 0 到 9 的值。

当字符具有 digit 属性时,使用 Character.digit()静态方法将其转换为相应的整数数值。请注意,digit()方法被重载以接受 char 或 int 参数。此外,该方法需要基数。基数的常用值是 2、10 和 16。有趣的是,虽然字符 A–F 和 A–F 没有 digit 属性,但它们可以用作基数为 16 的数字。对于这些字符,digit()方法返回 10 到 15 之间的预期整数值。

要完全理解 Unicode 字符集和 Java 的实现,需要熟悉几个新术语:字符、码位、字符、编码、序列化编码、UTF-8 和 UTF-16。这些术语超出了本菜谱的范围,但是您可以从位于 http://unicode.org 的 Unicode 网站或字符类 Java API 文档中了解更多关于这些和其他 Unicode 概念的信息。

12-2.创建和使用语言环境

问题

您希望以符合客户语言和文化期望的用户友好方式显示数字、日期和时间。

解决办法

数字、日期和时间的显示格式因世界而异,这取决于用户的语言和文化区域。此外,文本排序规则因语言而异。java.util.Locale 类表示世界上特定的语言和地区。通过确定和使用客户的区域设置,您可以将该区域设置应用于各种格式类,这些格式类可用于以预期的形式创建用户可见的数据。使用 Locale 实例来修改特定语言或地区的行为的类称为 locale-sensitive 类。你可以在第四章中了解更多关于区域敏感类的知识。该章向您展示了如何在 NumberFormat 和 DateFormat 类中使用 Locale 实例。然而,在本菜谱中,您将学习创建这些区域实例的不同选项。

您可以通过以下任何方式创建区域设置实例:

  • 使用区域设置。用于配置和构建区域设置对象的生成器类。

  • 使用静态 Locale.forLanguageTag()方法。

  • 使用区域设置构造函数创建对象。

  • 使用预先配置的静态语言环境对象。

Java 语言环境。构建器类具有 setter 方法,允许您创建可以转换为格式良好的最佳通用实践(BCP) 47 语言标记的区域设置。“它是如何工作的”一节更详细地描述了 BCP 47 标准。现在,您应该简单地理解构建器创建符合该标准的场所实例。

以下代码片段来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 2。Recipe12_2 类演示了如何创建生成器和区域设置实例。您可以在区分区域设置的类中使用所创建的区域设置来生成区域性正确的显示格式:

private static final long number = 123456789L;
private static final Date now = new Date();

private void createFromBuilder() {
    System.out.printf("Creating from Builder...\n\n");
    String[][] langRegions = {{"fr", "FR"}, {"ja", "JP"}, {"en", "US"}};        
    Builder builder = new Builder();
    Locale l = null;
    NumberFormat nf = null;
    DateFormat df = null;
    for (String[] lr: langRegions) {
        builder.clear();
        builder.setLanguage(lr[0]).setRegion(lr[1]);
        l = builder.build();
        nf = NumberFormat.getInstance(l);
        df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, l);
        System.out.printf("Locale: %s\nNumber: %s\nDate: %s\n\n",
            l.getDisplayName(),
            nf.format(number),
            df.format(now));     
    }

前面的代码将以下内容打印到标准控制台:

Creating from Builder...

Locale: French (France)
Number: 123 456 789
Date: 14 septembre 2016 00:08:06 PDT

Locale: Japanese (Japan)
Number: 123,456,789
Date: 2016/09/14 0:08:06 PDT

Locale: English (United States)
Number: 123,456,789
Date: September 14, 2016 12:08:06 AM PDT

创建 Locale 实例的另一种方法是使用静态 Locale.forLanguageTag()方法。此方法允许您使用 BCP 47 语言标记参数。以下代码使用 forLanguageTag()方法从相应的语言标记创建三个区域设置:

...        
System.out.printf("Creating from BCP 47 language tags...\n\n");
String[] bcp47LangTags= {"fr-FR", "ja-JP", "en-US"};        
Locale l = null;
NumberFormat nf = null;
DateFormat df = null;
for (String langTag: bcp47LangTags) {
    l = Locale.forLanguageTag(langTag);
    nf = NumberFormat.getInstance(l);
    df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, l);
    System.out.printf("Locale: %s\nNumber: %s\nDate: %s\n\n",
        l.getDisplayName(),
        nf.format(number),
        df.format(now));
}
...

输出类似于从生成器生成的区域设置实例创建的结果:

Creating from BCP 47 language tags...

Locale: French (France)
Number: 123 456 789
Date: 14 septembre 2016 01:07:22 PDT
...

还可以使用构造函数来创建实例。以下代码显示了如何做到这一点:

Locale l = new Locale("fr", "FR");

其他构造函数允许您传递更少或更多的参数。参数可以包括语言、地区和可选的变量代码。

最后,Locale 类有许多针对一些常用情况的预定义静态实例。因为实例是预定义的,所以您的代码只需要引用静态实例。例如,以下示例显示了如何引用表示 fr-FR、ja-JP 和 en-US 语言环境的现有静态实例:

Locale frenchInFrance = Locale.FRANCE;
Locale japaneseInJapan = Locale.JAPAN;
Locale englishInUS = Locale.US;

有关其他静态实例的示例,请参考 locale Java API 文档。

它是如何工作的

Locale 类为区分区域设置的类提供了执行符合区域性的数据格式化和解析所需的上下文。一些区分区域设置的类包括:

  • Java . text . number 格式

  • java.text.DateFormat

  • java.util.Calendar

一个 Locale 实例标识一种特定的语言,可以对其进行微调,以标识用特定脚本编写的语言或在特定地区使用的语言。对于创造任何依赖于语言或地域影响的事物来说,语言环境是一个重要且必要的元素。

Java Locale 类一直在增强,以便为现代 BCP 47 语言标签提供更好的支持。BCP 47 定义了在语言、地区、文字和变体标识符方面使用 ISO 标准的最佳实践。尽管现有的语言环境构造函数继续与 Java 平台的早期版本兼容,但是这些构造函数不支持额外的脚本标记。例如,只有最近添加的区域设置。Builder 类和 Locale.forLanguageTag()方法支持标识脚本的新功能。因为区域设置构造函数不强制严格遵守 BCP 47,所以您应该在任何新代码中避免使用这些构造函数。相反,开发人员应该使用 Builder 类和 forLanguageTag()方法。

一个地点。构建器实例具有多种 setter 方法,可帮助您对其进行配置,以创建有效的、符合 BCP 47 标准的区域设置实例:

  • 公共区域设置。BuildersetLanguage(字符串语言)

  • 公共区域设置。BuildersetRegion(字符串区域)

  • 当地观众。buildersetscript(字符串脚本)

如果这些方法的参数不是 BCP 47 标准的格式良好的元素,则每个方法都会引发 Java . util . illformedlocaleexception。language 参数必须是有效的两个或三个字母的 ISO 639 语言标识符。region 参数必须是有效的两个字母的 ISO 3166 地区代码或三个数字的 M.49 联合国“区域”代码。最后,脚本参数必须是有效的四字母 ISO 15924 脚本代码。

构建器允许您对其进行配置,以创建特定的符合 BCP 47 的语言环境。一旦设置了所有配置,build()方法就会创建并返回一个 Locale 实例。请注意,所有的 setters 可以被链接在一起形成一条语句。构建器模式的工作原理是让每个配置方法返回一个对当前实例的引用,在该实例上可以调用更多的配置方法。

Locale aLocale = new Builder().setLanguage("fr").setRegion("FR").build();

BCP 47 文件及其包含的标准可在以下位置找到:

12-3.设置默认区域设置

问题

您希望为所有区分区域设置的类设置默认区域设置。

解决办法

使用 Locale.setDefault()方法设置所有区分区域设置的类默认情况下将使用的区域设置实例。此方法由以下两种形式重载:

  • locale . set default(alocale locale)

  • locale . setdefault(本地)。c 类(locale alocale)

此示例代码演示了如何为所有区分区域设置的类设置默认区域设置:

Locale.setDefault(Locale.FRANCE);

您还可以为另外两个区域设置类别设置默认值,即显示和格式:

Locale.setDefault(Locale.Category.DISPLAY, Locale.US);
Locale.setDefault(Locale.Category.FORMAT, Locale.FR);

您可以在应用中创建使用这些特定区域设置类别的代码,以混合不同用途的区域设置选择。例如,您可以选择对 ResourceBundle 文本使用显示区域设置,而对日期和时间格式使用格式区域设置。org . Java 9 recipes . chapter 12 . recipe 12 _ 3 中的示例代码。Recipe12_3 类演示了这种更复杂的用法:

public class Recipe12_3 {
    private static final Date NOW = new Date();
    public void run() {
        // Set ALL locales to fr-FR
        Locale.setDefault(Locale.FRANCE);
        demoDefaultLocaleSettings();
        // System default is still fr-FR
        // DISPLAY default is es-MX
        // FORMAT default is en-US
        Locale.setDefault(Locale.Category.DISPLAY, Locale.forLanguageTag("es-MX"));
        Locale.setDefault(Locale.Category.FORMAT, Locale.US);
        demoDefaultLocaleSettings();
        // System default is still fr-FR
        // DISPLAY default is en-US
        // FORMAT default is es-MX
        Locale.setDefault(Locale.Category.DISPLAY, Locale.US);
        Locale.setDefault(Locale.Category.FORMAT, Locale.forLanguageTag("es-MX"));
        demoDefaultLocaleSettings();
        // System default is Locale.US
        // Resets both DISPLAY and FORMAT locales to en-US as well.
        Locale.setDefault(Locale.US);
        demoDefaultLocaleSettings();
    }
    public void demoDefaultLocaleSettings() {
        DateFormat df =
            DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
        ResourceBundle resource =
            ResourceBundle.getBundle("SimpleResources",
                Locale.getDefault(Locale.Category.DISPLAY));
        String greeting = resource.getString("GOOD_MORNING");
        String date = df.format(NOW);
        System.out.printf("DEFAULT LOCALE: %s\n", Locale.getDefault());
        System.out.printf("DISPLAY LOCALE: %s\n", Locale.getDefault(Locale.Category.DISPLAY));
        System.out.printf("FORMAT LOCALE:  %s\n", Locale.getDefault(Locale.Category.FORMAT));
        System.out.printf("%s, %s\n\n", greeting, date );
    }
    public static void main(String[] args) {
        Recipe12_3 app = new Recipe12_3();
        app.run();
    }
}

该代码产生以下输出:

DEFAULT LOCALE: fr_FR
DISPLAY LOCALE: fr_FR
FORMAT LOCALE:  fr_FR
Bonjour!, 19/09/16 20:31

DEFAULT LOCALE: fr_FR
DISPLAY LOCALE: es_MX
FORMAT LOCALE:  en_US
¡Buenos días!, 9/19/16 8:31 PM

DEFAULT LOCALE: fr_FR
DISPLAY LOCALE: en_US
FORMAT LOCALE:  es_MX
Good morning!, 19/09/16 08:31 PM

DEFAULT LOCALE: en_US
DISPLAY LOCALE: en_US
FORMAT LOCALE:  en_US
Good morning!, 9/19/16 8:31 PM

它是如何工作的

Locale 类允许您为两个不同的类别设置默认的区域设置。类别由地区表示。类别枚举:

  • 区域设置。类别。显示

  • 区域设置。类别.格式

使用应用用户界面的显示类别。设置默认显示区域设置意味着 ResourceBundle 类可以独立于格式区域设置为该特定区域设置加载用户界面资源。设置格式默认区域设置会影响各种格式子类的行为。例如,默认的 DateFormat 实例将使用 Format 默认区域设置来创建区分区域设置的输出格式。同样,这两个类别是独立的,因此您可以针对不同的需求使用不同的区域设置实例。

在这个菜谱的示例代码中,Locale.setDefault(Locale。法国)方法调用将默认的系统、显示和格式区域设置设置为 fr-FR(法国的法语)。此方法总是重置显示和格式区域设置,以匹配系统区域设置。创建新的资源包时,resource bundle 类默认使用系统区域设置。但是,通过提供一个 locale 实例参数,您可以告诉 bundle 为特定的 Locale 加载资源。例如,即使系统区域设置是 locale。法国,您可以指定显示默认区域设置,并在 ResourceBundle.getBundle()方法调用中使用该显示区域设置。例如,这段代码试图为 es-MX 加载一个语言包,即使系统语言环境仍然是 locale。法国:

Locale.setDefault(Locale.Category.DISPLAY, Locale.forLanguageTag("es-MX"));
Locale.setDefault(Locale.Category.FORMAT, Locale.US);
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
ResourceBundle resource =
        ResourceBundle.getBundle("org.java9recipes.chapter12.resource.SimpleResources",
                Locale.getDefault(Locale.Category.DISPLAY));
String greeting = resource.getString("GOOD_MORNING");

在这种情况下,它找到一个带有“Buenos días!”的早安资源值,因为显示默认区域设置是一个参数。资源包是一个文件,其中包含各种语言环境的已翻译属性字符串。名为 SimpleResources_en.properties(英语)的文件有一个 GOOD_MORNING 属性,写为“Good morning!”请注意,资源包中每个属性的翻译必须存在于特定于区域设置的资源文件中才能显示。Java 代码不翻译这些字符串。相反,它只是根据所选的语言环境选择所需属性的适当翻译。

注意

虽然如果您不在 DateFormat 和 NumberFormat 类的创建方法中提供区域设置参数,它们将自动使用默认的格式区域设置,但 ResourceBundle.getBundle()方法在默认情况下始终使用系统区域设置。要在 ResourceBundle()中使用显示默认区域设置,必须将其作为参数显式提供。

12-4.匹配和过滤区域设置

问题

您希望匹配或过滤区域设置列表,并只返回符合指定条件的区域设置。

解决办法

利用 Java 8 的 java.util.Locale 类中引入的新的语言环境匹配和过滤方法。如果您得到一个字符串格式的逗号分隔的区域设置列表,您可以对该字符串应用过滤器或“优先级列表”,以便只返回字符串中符合过滤器的区域设置。在以下示例中,使用 java.util.Locale filterTag 方法筛选语言标记列表,以字符串格式返回匹配的标记:

List<Locale.LanguageRange> list1 = Locale.LanguageRange.parse("ja-JP, en-US");
list1.stream().forEach((range) -> {
    System.out.println("Range:" + range.getRange());
});
ArrayList localeList = new ArrayList();
localeList.add("en-US");
localeList.add("en-JP");

List<String> tags1 = Locale.filterTags(list1, localeList);
System.out.println("The following is the filtered list of locales:");
tags1.stream().forEach((tag) -> {
    System.out.println(tag);
});

结果:

Range:ja-jp
Range:en-us
The following is the filtered list of Locales:
en-us

Locale 类的 filter()方法允许您返回匹配的 Locale 实例的列表。在下面的示例中,区域语言标记列表用于从区域列表中筛选区域类。

String localeTags = Locale.ENGLISH.toLanguageTag() + "," +
                    Locale.CANADA.toLanguageTag();
List<Locale.LanguageRange> list1 = Locale.LanguageRange.parse(localeTags);
list1.stream().forEach((range) -> {
    System.out.println("Range:" + range.getRange());
});
ArrayList<Locale> localeList = new ArrayList();
localeList.add(new Locale("en"));
localeList.add(new Locale("en-JP"));

List<Locale> tags1 = Locale.filter(list1, localeList);
System.out.println("The following is the matching list of Locales:");
tags1.stream().forEach((tag) -> {
    System.out.println(tag);
});

结果如下:

Range:en
Range:en-ca
The following is the matching list of locales:
en

它是如何工作的

Java 8 中的 java.util.Locale 类中添加了一些方法,允许您基于 List <locale.languagerange>格式的优先级列表过滤语言环境实例或语言标签。过滤机制基于 RFC 4647。以下列表包含这些过滤方法的简短摘要:</locale.languagerange>

  • 过滤器(列表<locale.languagerange>,集合</locale.languagerange>

    filter(List<Locale.LanguageRange>, Collection<Locale>, Locale.FilteringMode)
    

    (返回区域设置实例的匹配列表)

  • filterTags(列表<locale.languagerange>,集合</locale.languagerange>

    filterTags(List<Locale.LanguageRange>, Collection<String>, Locale.FilteringMode)
    

    (返回匹配的语言标签列表)

要使用每种方法,应该将排序后的优先级顺序作为第一个参数发送。这个优先级顺序是一个区域列表。LanguageRange 对象,并且应该根据优先级或权重按降序排序。filter()方法中的第二个参数是区域设置的集合。此集合包含将被筛选的区域设置。可选的第三个参数包含一个 Locale.FilteringMode。表 12-1 列出了不同的过滤模式。

表 12-1。区域设置。过滤模式值
|

模式

|

描述

|
| --- | --- |
| 自动选择 _ 过滤 | 指定基于给定优先级语言列表的筛选模式。 |
| 扩展 _ 过滤 | 指定扩展筛选。 |
| 忽略 _ 扩展 _ 范围 | 指定基本筛选。 |
| 地图 _ 扩展 _ 范围 | 指定基本筛选,如果语言优先级列表中包含任何扩展语言,它们将被映射到基本语言范围。 |
| 拒绝 _ 扩展 _ 范围 | 指定基本筛选,如果语言优先级列表中包含任何扩展语言,该列表将被拒绝并引发 IllegalArgumentException。 |

12-5.使用正则表达式搜索 Unicode

问题

您希望在字符串中查找或匹配 Unicode 字符。您希望使用正则表达式语法来实现这一点。

解决方案 1

查找或匹配字符的最简单方法是使用 String 类本身。字符串实例存储 Unicode 字符序列,并使用正则表达式提供相对简单的查找、替换和标记字符的操作。

若要确定字符串是否匹配正则表达式,请使用 matches()方法。如果整个字符串与正则表达式完全匹配,matches()方法返回 true。

以下代码来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 4。Recipe12_4 类使用两个不同的表达式和两个字符串。正则表达式匹配只是确认字符串匹配在变量 enRegEx 和 jaRegEx 中定义的特定模式。

private String enText = "The fat cat sat on the mat with a brown rat.";
private String jaText = "Fight 文字化け!";

boolean found = false;
String enRegEx = "^The \\w+ cat.*";
String jaRegEx = ".*文字.*";
String jaRegExEscaped = ".*\u6587\u5B57.*";
found = enText.matches(enRegEx);
if (found) {
    System.out.printf("Matches %s.\n", enRegEx);
}        
found = jaText.matches(jaRegEx);
if (found) {
    System.out.printf("Matches %s.\n", jaRegEx);
}
found = jaText.matches(jaRegExEscaped);
if (found) {
    System.out.printf("Matches %s.\n", jaRegExEscaped);
}

该代码打印以下内容:

Matches ^The \w+ cat.*.
Matches .*文字.*.
Matches .*文字.*.

使用 replaceFirst()方法创建一个新的 String 实例,其中目标文本中正则表达式的第一个匹配项被替换为替换文本。该代码演示了如何使用此方法:

String replaced = jaText.replaceFirst("文字化け", "mojibake");
System.out.printf("Replaced: %s\n", replaced);

输出中显示了替换文本:

Replaced: Fight mojibake!

replaceAll()方法用替换文本替换所有出现的表达式。

最后,split()方法创建一个 String[],其中包含由匹配表达式分隔的文本。换句话说,它返回由表达式分隔的文本。或者,您可以提供一个 limit 参数来限制在源文本中应用分隔符的次数。以下代码演示了 split()方法对空格字符进行拆分:

String[] matches = enText.split("\\s", 3);
for(String match: matches) {
    System.out.printf("Split: %s\n",match);
}

代码的输出如下:

Split: The
Split: fat
Split: cat sat on the mat with a brown rat.

解决方案 2

当简单的字符串方法不够时,可以使用更强大的 java.util.regex 包来处理正则表达式。使用 Pattern 类创建正则表达式。匹配器使用模式处理字符串实例。所有匹配器操作都使用模式和字符串实例来执行它们的功能。

下面的代码演示了如何在两个单独的字符串中搜索 ASCII 和非 ASCII 文本。见 org . Java 9 recipes . chapter 12 . recipe 12 _ 4。Recipe12_4 类的完整源代码。demoSimple()方法查找后面跟有任何字符的文本。在”。demoComplex()方法在字符串中查找两个日语符号:

public void demoSimple() {
Pattern p = Pattern.compile(".at");
    Matcher m = p.matcher(enText);
    while(m.find()) {
        System.out.printf("%s\n", m.group());
    }
}

public void demoComplex() {
    Pattern p = Pattern.compile("文字");
    Matcher m = p.matcher(jaText);
    if (m.find()) {
        System.out.println(m.group());
    }
}

对先前定义的英语和日语文本运行这两种方法会显示以下内容:

fat
cat
sat
mat
rat
文字

它是如何工作的

使用正则表达式的字符串方法如下:

  • 公共布尔匹配(字符串正则表达式)

  • public String replaceFirst(字符串正则表达式,字符串替换)

  • public String replaceAll(字符串正则表达式,字符串替换)

  • 公共 String[] split(String regex,int limit)

  • 公共字符串[]拆分(字符串正则表达式)

字符串方法是 java.util.regex 类更强大功能的有限且相对简单的包装:

  • java.util.regex .模式

  • java.util.regex.Matcher

  • Java . util . regex . patternantxeexception

Java 正则表达式类似于 Perl 语言中使用的那些表达式。虽然关于 Java 正则表达式还有很多要学的,但是从这个食谱中最重要的几点可能是:

  • 正则表达式肯定可以包含所有 Unicode 字符中的非 ASCII 字符。

  • 由于 Java 语言编译器理解反斜杠字符的特性,您将不得不在代码中使用两个反斜杠,而不是一个用于预定义的字符类表达式。

在正则表达式中使用非 ASCII 字符最方便、最易读的方法是使用键盘输入法将它们直接输入到源文件中。操作系统和编辑器允许您在 ASCII 之外输入复杂文本的方式有所不同。不管什么操作系统,如果你的编辑允许,你都应该用 UTF-8 编码保存文件。作为使用非 ASCII 正则表达式的另一种更困难的方法,您可以使用\uXXXX 符号对字符进行编码。使用这种表示法,您可以输入 \u\U ,后跟 Unicode 码位的十六进制表示,而不是使用键盘直接键入字符。这个菜谱的代码示例使用了日语单词“文字”(发音墨姬)。如示例所示,您可以在正则表达式中使用实际字符,也可以查找 Unicode 码位值。对于这个特定的日语单词,编码将是\u6587\u5B57。

Java 语言的正则表达式支持包括特殊的字符类。例如,\d 和\w 分别是正则表达式[0-9]和[a-zA-z0-9]的快捷表示法。但是,由于 Java 编译器对反斜杠字符的特殊处理,在使用预定义的字符类如\d(数字)、\w(单词字符)和\s(空格字符)时,必须使用额外的反斜杠。例如,要在源代码中使用它们,您可以分别输入 \d\w\s 。示例代码在解决方案 1 中使用双反斜杠来表示\w 字符类:

String enRegEx = "^The                  **\\w**                  + cat.*"; 

12-6.覆盖默认货币

问题

您希望使用与默认区域设置无关的货币来显示数值。

解决办法

通过显式设置 NumberFormat 实例中使用的货币,控制使用格式化货币值打印哪种货币。以下示例假定默认区域设置为 Locale.JAPAN。它通过调用 NumberFormat 实例的 setCurrency(Currency c)方法来更改货币。这个例子来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 6。Recipe12_6 类。

BigDecimal value = new BigDecimal(12345);
System.out.printf("Default locale: %s\n", Locale.getDefault().getDisplayName());
NumberFormat nf = NumberFormat.getCurrencyInstance();
String formattedCurrency = nf.format(value);
System.out.printf("%s\n", formattedCurrency);
Currency c = Currency.getInstance(Locale.US);
nf.setCurrency(c);
formattedCurrency = nf.format(value);
System.out.printf("%s\n\n", formattedCurrency);

前面的代码打印出以下内容:

Default locale: 日本語 (日本)
¥12,345
USD12,345

它是如何工作的

使用 NumberFormat 实例来格式化货币值。您应该显式调用 getCurrencyInstance()方法来创建货币格式化程序:

NumberFormat nf = NumberFormat.getCurrencyInstance();

前一个格式化程序将使用您的默认区域设置首选项将数字格式化为货币值。此外,它将使用与地区相关联的货币符号。然而,一个非常常见的用例涉及到为不同地区的货币设置值的格式。

使用 setCurrency()方法在数字格式化程序中显式设置货币:

nf.setCurrency(aCurrencyInstance); // requires a Currency instance

请注意,java.util.Currency 类是一个工厂。它允许您以两种方式创建货币对象:

  • Currency.getInstance(本地端)

  • Currency.getInstance(字符串货币代码)

第一个 getInstance 调用使用一个 Locale 实例来检索货币对象。Java 平台将默认货币与地区相关联。在这种情况下,当前与美国相关联的默认货币是美元:

Currency c1 = Currency.getInstance(Locale.US);

第二个 getInstance 调用使用有效的 ISO 4217 货币代码。美元的货币代码是 USD:

Currency c2 = Currency.getInstance("USD");

一旦有了货币实例,只需在格式化程序中使用该实例:

nf.setCurrency(c2);

这个格式化程序现在被配置为使用默认区域设置的数字格式符号和模式来格式化数字值,但是它会将目标货币代码显示为可显示文本的一部分。这允许您将默认的数字格式模式与其他货币代码混合使用。

注意

货币既有符号又有代码。货币代码总是指三个字母的 ISO 4217 代码。货币符号通常不同于代码。例如,美元的代码为 USD,符号为$。货币格式化程序在使用默认区域设置中的地区货币格式化数字时,通常会使用符号。但是,当您显式更改格式化程序的货币时,格式化程序并不总是知道目标货币的本地化符号。在这种情况下,format 实例通常会在显示的文本中使用货币代码。

12-7.将字节数组与字符串相互转换

问题

您需要将字节数组中的字符从传统字符集编码转换为 Unicode 字符串。

解决办法

使用 String 类将传统字符编码从字节数组转换为 Unicode 字符串。以下代码片段来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 7。Recipe12_7 类演示了如何将传统的移位 JIS 编码的字节数组转换为字符串。在同一示例的后面,代码演示了如何从 Unicode 转换回移位 JIS 字节数组。

byte[] legacySJIS = {(byte)0x82,(byte)0xB1,(byte)0x82,(byte)0xF1,
(byte)0x82,(byte)0xC9,(byte)0x82,(byte)0xBF,
(byte)0x82,(byte)0xCD,(byte)0x81,(byte)0x41,
(byte)0x90,(byte)0xA2,(byte)0x8A,(byte)0x45,
(byte)0x81,(byte)0x49};

// Convert a byte[] to a String
Charset cs =Charset.forName("SJIS");
String greeting = new String(legacySJIS, cs);
System.out.printf("Greeting: %s\n", greeting);

这段代码打印出转换后的文本,即“Hello,world!”在日语中:

Greeting: こんにちは、世界!

使用 getBytes()方法将字符从字符串转换为字节数组。在前面代码的基础上,使用以下代码转换回原始编码,并比较结果:

// Convert a String to a byte[]
byte[] toSJIS = greeting.getBytes(cs);

// Confirm that the original array and newly converted array are same
Boolean same = false;
if (legacySJIS.length == toSJIS.length) {
    for (int x=0; x< legacySJIS.length; x++) {
        if(legacySJIS[x] != toSJIS[x]) break;
    }
    same = true;
}
System.out.printf("Same: %s\n", same.toString());

正如预期的那样,输出表明返回到遗留编码的往返转换是成功的。原始字节数组和转换后的字节数组包含相同的字节:

Same: true

它是如何工作的

Java 平台为许多传统字符集编码提供了转换支持。当从字节数组创建字符串实例时,必须向字符串构造函数提供一个 charset 参数,以便平台知道如何执行从传统编码到 Unicode 的映射。所有 Java 字符串都使用 Unicode 作为本地编码。

原始数组中的字节数通常不等于结果字符串中的字符数。在这个配方的例子中,原始数组包含 18 个字节。移位 JIS 编码需要 18 个字节来表示日语文本。但是,转换后,结果字符串包含九个字符。字节和字符之间没有 1:1 的关系。在这个例子中,在原始的移位 JIS 编码中,每个字符需要两个字节。

实际上有数百种不同的字符集编码。编码的数量取决于您的 Java 平台实现。但是,您可以保证支持几种最常见的编码,并且您的平台很可能包含比这个最小集合更多的编码:

  • 美国-阿斯凯

  • ISO-8859-1

  • UTF-8

  • UTF-16BE

  • UTF-16LE 编码

  • UTF-16

构建字符集时,您应该准备好处理字符集不受支持时可能出现的异常:

  • 当字符集名称不合法时抛出

  • 当字符集名称为空时抛出

  • 当你的 JVM 不支持目标字符集时抛出

12-8.转换字符流和缓冲区

问题

您需要在大块 Unicode 字符文本和任意面向字节的编码之间进行转换。大块文本可能来自流或文件。

解决方案 1

使用 java.io.InputStreamReader 将字节流解码为 Unicode 字符。使用 java.io.OutputStreamWriter 将 Unicode 字符编码为字节流。

下面的代码使用 InputStreamReader 从类路径中的文件读取并转换可能很大的文本字节块。org . Java 9 recipes . chapter 12 . recipe 12 _ 8。StreamConversion 类为此示例提供了完整的代码:

public String readStream() throws IOException {
    InputStream is = getClass().getResourceAsStream("resource/helloworld.sjis.txt");
    StringBuilder sb = new StringBuilder();
    if (is != null) {
        try (InputStreamReader reader =
                new InputStreamReader(is, Charset.forName("SJIS"))) {
            int ch = reader.read();
            while (ch != -1) {
                sb.append((char) ch);
                ch = reader.read();
            }
        }
    }
    return sb.toString();
}

类似地,可以使用 OutputStreamWriter 将文本写入字节流。下面的代码将一个字符串写入 UTF 8 编码的字节流:

public void writeStream(String text) throws IOException {
    FileOutputStream fos = new FileOutputStream("helloworld.utf8.txt");
    try (OutputStreamWriter writer
            = new OutputStreamWriter(fos, Charset.forName("UTF-8"))) {
        writer.write(text);
    }
}

解决方案 2

使用 Java . nio . charset . charset encoder 和 Java . nio . charset . charset decoder 在 Unicode 字符缓冲区和字节缓冲区之间进行转换。使用 newEncoder()或 newDecoder()方法从 charset 实例中检索编码器或解码器。然后使用编码器的 encode()方法创建字节缓冲区。使用解码器的 decode()方法创建字符缓冲区。以下代码来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 8。BufferConversion 类对缓冲区中的字符集进行编码和解码:

    public ByteBuffer encodeBuffer(String charsetName, CharBuffer charBuffer)
            throws CharacterCodingException {
        Charset charset = Charset.forName(charsetName);
CharsetEncoder encoder = charset.newEncoder();
        ByteBuffer targetBuffer = encoder.encode(charBuffer);
return targetBuffer;

    }
    public CharBuffer decodeBuffer(String charsetName, ByteBuffer srcBuffer)
            throws CharacterCodingException {
        Charset charset = Charset.forName(charsetName);
        CharsetDecoder decoder = charset.newDecoder();
        CharBuffer charBuffer = decoder.decode(srcBuffer);
        return charBuffer;
    }

它是如何工作的

java.io 和 java.nio.charset 包包含几个类,可以帮助您对大型文本流或缓冲区执行编码转换。流是方便的抽象,可以帮助您使用各种源和目标来转换文本。流可以表示 HTTP 连接中的传入或传出文本,甚至可以表示文件。

如果您使用 InputStream 来表示基础源文本,您将在 InputStreamReader 中包装该流,以执行从字节流的转换。读取器实例执行从字节到 Unicode 字符的转换。

使用 OutputStream 实例表示目标文本,将流包装在 OutputStreamWriter 中。编写器会在目标流中将您的 Unicode 文本转换为面向字节的编码。

为了有效地使用 OutputStreamWriter 或 InputStreamReader,您必须知道目标或源文本的字符编码。当您使用 OutputStreamWriter 时,源文本总是 Unicode,并且您必须提供一个 charset 参数来告诉编写器如何转换为目标面向字节的文本编码。使用 InputStreamReader 时,目标编码始终是 Unicode。您必须提供源文本编码作为参数,以便读者理解如何转换文本。

注意

Java 平台的字符串表示 Unicode 的 UTF-16 编码中的字符。Unicode 可以有几种编码,包括 UTF-16、UTF-8,甚至 UTF-32。在本讨论中,转换为 Unicode 始终意味着转换为 UTF-16。转换成面向字节的编码通常意味着转换成传统的非 Unicode 字符集编码。然而,一种常见的面向字节的编码是 UTF-8,使用 InputStreamReader 或 OutputStreamWriter 类将 Java 的“本机”UTF-16 Unicode 字符转换为 UTF-8 或从-8 转换是完全合理的。

执行编码转换的另一种方式是使用 CharsetEncoder 和 CharsetDecoder 类。CharsetEncoder 会将您的 Unicode CharBuffer 实例编码为 ByteBuffer 实例。CharsetDecoder 将 ByteBuffer 实例解码成 CharBuffer 实例。无论哪种情况,都必须提供一个字符集参数。

字符集表示在互联网签名号码管理机构(IANA)字符集注册表中定义的字符集编码。创建字符集实例时,应该使用注册表定义的字符集的规范名称或别名。你可以在www.iana.org/assignments/character-sets找到注册表。

请记住,您的 Java 实现不一定支持所有的 IANA 字符集名称。然而,所有的实现都需要至少支持本章的方法 12-7 中显示的那些。

12-9.设置区分区域设置的服务的搜索顺序

问题

您希望在 Java 运行时环境中为语言环境敏感的服务指定一个指定的搜索顺序。

解决办法

使用 java.locale.providers 属性为区分区域设置的服务指定所需的顺序。在下面的示例中,SPI 和 CLDR 提供程序是在属性中指定的。

java.locale.providers=SPI,CLDR

它是如何工作的

自 Java 8 发布以来,设置 java.locale.providers 属性指定了对语言环境敏感的服务的搜索顺序。该属性在 Java 运行时启动时读取。要设置服务顺序,请指定缩写,用逗号分隔。以下服务可供使用:

  • SPI:由 SPI(服务提供者接口)提供者表示的对地区敏感的服务

  • JRE:Java 运行时环境中的区域敏感服务

  • CLDR:基于 Unicode Consortium 的 CLDR 项目的提供商

  • 主机:反映底层操作系统中用户自定义设置的提供者

摘要

国际化是开发文化响应应用的关键。它允许更改应用文本,以符合应用使用的文化和语言。本章提供了一些例子,说明如何利用国际化技术来克服跨文化开发的细微差别。本章还介绍了有关 Unicode 转换的主题。

十三、使用数据库

几乎所有重要的应用都包含某种数据库。一些应用使用内存数据库,而其他应用使用传统的关系数据库管理系统(RDBMSs)。无论是哪种情况,每个 Java 开发人员都必须掌握一些使用数据库的技能。多年来,Java 数据库连接(JDBC) API 已经有了很大的发展,在过去的几个版本中已经有了一些重大的进步。

本章讲述了使用 JDBC 处理数据库的基础知识。您将学习如何执行所有标准的数据库操作,以及一些操作数据的高级技术。您还将了解如何使用 API 中的一些最新进展来创建安全的数据库应用并节省开发时间。最终,您将能够开发与 Oracle 数据库、PostgreSQL 和 MySQL 等传统 RDBMSs 一起工作的 Java 应用。

注意

要遵循本章中的示例,请运行 create_user.sql 脚本来创建数据库用户模式。然后,在刚刚创建的数据库模式中运行 create_database.sql 脚本。

本书中的数据库示例是为 Apache Derby 或 Oracle 数据库量身定制的,但是它们可以修改为适用于任何关系数据库。

13-1.连接到数据库

问题

您希望从桌面 Java 应用中创建一个到数据库的连接。

解决方案 1

使用 JDBC 连接对象来获取连接。为此,创建一个新的连接对象,然后加载您需要用于特定数据库供应商的驱动程序。一旦连接对象准备就绪,就调用它的 getConnection()方法。下面的代码演示了如何根据指定的驱动程序获得到 Oracle 或 Apache Derby 数据库的连接。

public Connection getConnection() throws SQLException {
    Connection conn = null;
    String jdbcUrl;
    if(driver.equals("derby")){
        jdbcUrl = "jdbc:derby://" + this.hostname + ":" +
                    this.port  + "/" + this.database;
    } else  {
        jdbcUrl = "jdbc:oracle:thin:@" + this.hostname + ":" +
                    this.port  + ":" + this.database;
    }
    System.out.println(jdbcUrl);
    conn = DriverManager.getConnection(jdbcUrl, username, password);
    System.out.println("Successfully connected");
    return conn;
}

本例中描述的方法返回一个准备好用于数据库访问的连接对象。

解决方案 2

使用数据源创建连接池。DataSource 对象必须已经正确实现并部署到应用服务器环境中。在实现和部署 DataSource 对象之后,应用可以使用它来获得到数据库的连接。以下代码显示了可用于通过 DataSource 对象获取数据库连接的代码:

public Connection getDSConnection() {
    Connection conn = null;
    try {
        Context ctx = new InitialContext();
        DataSource ds = (DataSource)ctx.lookup("jdbc/myOracleDS");
        conn = ds.getConnection();

    } catch (NamingException | SQLException ex) {
        ex.printStackTrace();
    }
    return conn;
}

注意,DataSource 实现中唯一需要的信息是有效 DataSource 对象的名称。获得数据库连接所需的所有信息都在应用服务器中管理。

它是如何工作的

在 Java 应用中,有几种不同的方法可以创建到数据库的连接。如何做到这一点取决于您正在编写的应用的类型。如果一个应用是独立的或者是一个桌面应用,那么经常使用 DriverManager。基于 Web 和 intranet 的应用通常依靠应用服务器通过 DataSource 对象为应用提供连接。

创建 JDBC 连接需要几个步骤。首先,您需要确定您将需要哪个数据库驱动程序。在确定了需要哪个驱动程序之后,下载包含该驱动程序的 JAR 文件,并将其放入类路径中。对于这个菜谱,要么建立 Oracle 数据库连接,要么建立 Apache Derby 连接。每个数据库供应商都将提供不同的 JDBC 驱动程序,这些驱动程序打包在具有不同名称的 JAR 文件中;有关更多信息,请参考特定数据库的文档。一旦获得了适合您的数据库的 JAR 文件,就将它包含在您的应用类路径中。接下来,使用 JDBC 驱动程序管理器获得到数据库的连接。从 4.0 版开始,类路径中包含的驱动程序被自动加载到 DriverManager 对象中。如果您使用的是 4.0 之前的 JDBC 版本,则必须手动加载驱动程序。

要使用 DriverManager 获得到数据库的连接,需要向它传递一个包含 JDBC URL 的字符串。JDBC URL 由数据库供应商名称、托管数据库的服务器名称、数据库名称、数据库端口号以及可以访问您要使用的模式或数据库对象的有效数据库用户名和口令组成。很多时候,用于创建 JDBC URL 的值都是从属性文件中获取的,因此如果需要的话,可以很容易地对它们进行更改。要了解更多关于使用属性文件存储连接值的信息,请参阅配方 13-5。用于为解决方案 1 创建 Oracle 数据库 JDBC URL 的代码如下所示:

String jdbcUrl = "jdbc:oracle:thin:@" + this.hostname + ":" +
                    this.port  + ":" + this.database;

一旦所有的变量都被替换到字符串中,它将看起来像下面这样:

jdbc:oracle:thin:@hostname:1521:database

类似地,Apache Derby URL 字符串如下所示:

jdbc:derby://hostname:1521/database

一旦创建了 JDBC URL,就可以将它传递给 DriverManager.getConnection()方法以获得 java.sql.Connection 对象。如果向 getConnection()方法传递了不正确的信息,将引发 Java . SQL . sqlexception;否则,将返回有效的连接对象。

获得数据库连接的首选方法是在应用服务器上运行时使用数据源,或者访问 Java 命名和目录接口(JNDI)服务。要使用 DataSource 对象,您需要有一个应用服务器将它部署到。任何兼容的 Java 应用服务器如 GlassFish、Oracle Weblogic、Payara 或 WildFly 都可以工作。大多数应用服务器都包含一个 web 接口,可以用来轻松部署 DataSource 对象。但是,您可以使用类似如下的代码来手动部署 DataSource 对象:

org.java9recipes.chapter13.recipe13_01.FakeDataSourceDriver ds =
        new org.java9recipes.chapter13.recipe13_1.FakeDataSourceDriver();
ds.setServerName("my-server");
ds.setDatabaseName("JavaRecipes");
ds.setDescription("Database connection for Java 9 Recipes");

这段代码实例化一个新的 DataSource 驱动程序类,然后根据您想要注册的数据库设置属性。在应用服务器中注册数据源或访问 JNDI 服务器时,通常会使用这里演示的数据源代码。如果您使用基于 web 的管理工具来部署数据源,应用服务器通常在幕后完成这项工作。大多数数据库供应商都会提供一个数据源驱动程序以及他们的 JDBC 驱动程序,所以如果正确的 JAR 驻留在应用或服务器类路径中,它应该可以被识别并可供使用。一旦实例化和配置了数据源,下一步就是向 JNDI 命名服务注册数据源。

以下代码演示了向 JNDI 注册数据源的过程:

try {
    Context ctx = new InitialContext();
    DataSource ds =
            (DataSource) ctx.bind("java9recipesDB");
} catch (NamingException ex) {
    ex.printStackTrace();
}

一旦部署了数据源,部署到同一应用服务器的任何应用都可以访问它。使用 DataSource 对象的美妙之处在于,您的应用代码不需要了解数据库的任何信息;它只需要知道数据源的名称。通常,数据源的名称以 jdbc/前缀开头,后面跟一个标识符。为了查找 DataSource 对象,使用了 InitialContext。InitialContext 查看应用服务器中所有可用的数据源,如果找到,则返回有效的数据源;否则会抛出 java.naming.NamingException 异常。在解决方案 2 中,您可以看到 InitialContext 返回一个必须转换为 DataSource 的对象。

Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup("jdbc/myOracleDS");

如果数据源是连接池缓存,当应用请求时,它将发送连接池中的一个可用连接。以下代码行从数据源返回一个连接对象:

conn = ds.getConnection();

当然,如果不能获得有效的连接,就会抛出 java.sql.SQLException。DataSource 技术优于 DriverManager,因为数据库连接信息只存储在一个地方:应用服务器。一旦部署了有效的数据源,它就可以被许多应用使用。

在您的应用获得一个有效的连接之后,就可以使用它来处理数据库了。要了解有关使用连接对象处理数据库的更多信息,请参见方法 13-2 和 13-4。

13-2.处理连接和 SQL 异常

问题

应用中的数据库活动引发了异常。您需要处理 SQL 异常,以便您的应用不会崩溃。

解决办法

使用 try-catch 块来捕获和处理由 JDBC 连接或 SQL 查询引发的任何 SQL 异常。下面的代码演示了如何实现 try-catch 块来捕获 SQL 异常:

try {
    // perform database tasks
} catch (java.sql.SQLException){
   // perform exception handling
}

它是如何工作的

标准的 try-catch 块可用于捕获 java.sql.Connection 或 java.sql.SQLException 异常。如果不处理这些异常,您的代码将无法编译,为了防止您的应用在这些异常之一被抛出时崩溃,适当地处理它们是一个好主意。几乎所有针对 java.sql.Connection 对象执行的工作都需要包含错误处理,以确保正确处理数据库异常。事实上,通常需要嵌套的 try-catch 块来处理所有可能的异常。您需要确保一旦完成工作并且不再使用连接对象,就关闭连接。同样,关闭 java.sql.Statement 对象来清理内存分配也是一个好主意。

因为需要关闭语句和连接对象,所以经常会看到使用 try-catch-finally 块来确保所有资源都被按需使用。您很可能会看到类似以下样式的旧 JDBC 代码:

try {
    // perform database tasks
} catch (java.sql.SQLException ex) {
    // perform exception handling
} finally {
    try {
        // close Connection and Statement objects
    } catch (java.sql.SQLException ex){
        // perform exception handling
    }
}

应该编写新的代码来利用 try-with-resources 语句,该语句允许将资源管理卸载到 Java,而不是执行手动关闭。下面的代码演示如何使用 try-with-resources 打开连接,创建语句,然后在完成后关闭连接和语句。

注意

示例中的 createConn 对象抽象出了获取数据库连接的细节,这些细节可以通过调用 getConnection()方法返回。

try (Connection conn = createConn.getConnection();
        Statement stmt = conn.createStatement();) {
    ResultSet rs = stmt.executeQuery(qry);
    while (rs.next()) {
        // PERFORM SOME WORK
    }
} catch (SQLException e) {
    e.printStackTrace();
}

如前面的伪代码所示,为了清理未使用的资源,经常需要嵌套的 try-catch 块。适当的异常处理有时会使 JDBC 代码编写起来相当费力,但它也将确保需要数据库访问的应用不会失败,从而导致数据丢失。

13-3.查询数据库和检索结果

问题

应用中的一个进程需要查询数据库表中的数据。

解决办法

使用方法 13-1 中描述的技术之一获得一个 JDBC 连接,然后使用 java.sql.Connection 对象创建一个语句对象。java.sql.Statement 对象包含 executeQuery()方法,该方法解析文本字符串并使用它来查询数据库。一旦执行了查询,就可以将查询结果检索到 ResultSet 对象中。以下示例查询名为 RECIPES 的数据库表并打印结果:

String qry = "select recipe_num, name, description from recipes";
try (Connection conn = createConn.getConnection();
        Statement stmt = conn.createStatement();) {
    ResultSet rs = stmt.executeQuery(qry);
    while (rs.next()) {
        String recipe = rs.getString("RECIPE_NUM");
        String name = rs.getString("NAME");
        String desc = rs.getString("DESCRIPTION");

        System.out.println(recipe + "\t" + name + "\t" + desc);
    }
} catch (SQLException e) {
    e.printStackTrace();
}

如果您使用本章中包含的数据库脚本执行此代码,您将收到以下结果:

13-1    Connecting to a Database         DriverManager and DataSource Implementations
13-2    Querying a Database and Retrieving Results      Obtaining and Using Data from a DBMS
13-3    Handling SQL Exceptions Using SQLException

它是如何工作的

对数据库最常执行的操作之一是查询。使用 JDBC 执行数据库查询非常容易,尽管每次执行查询时都需要使用一些样板代码。首先,您需要为您想要运行查询的数据库和模式获取一个连接对象。你可以通过使用配方 13-1 中的一个解决方案来完成。接下来,您需要形成一个查询并以字符串格式存储它。然后,连接对象用于创建语句。您的查询字符串将被传递给语句对象的 executeQuery()方法,以便实际查询数据库。在这里,您可以看到不使用 try-with-resources 进行资源管理时的情况。

String qry = "select recipe_num, name, description from recipes";
Connection conn;
Statement stmt = null;

try {
    conn = createConn.getConnection()
    stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery(qry);
...

相同的代码可以更有效地编写如下:

try (Connection conn = createConn.getConnection();
        Statement stmt = conn.createStatement();) {
    ResultSet rs = stmt.executeQuery(qry);
...

如您所见,语句对象的 executeQuery()方法接受一个字符串并返回一个 ResultSet 对象。ResultSet 对象使处理查询结果变得容易,因此您可以按任何顺序获得所需的信息。如果您看一下示例中的下一行代码,就会发现在 ResultSet 对象上创建了一个 while 循环。这个循环将继续调用 ResultSet 对象的 next()方法,获得每次迭代从查询中返回的下一行。在这种情况下,ResultSet 对象被命名为 rs,因此当 rs.next()返回 true 时,循环将继续被处理。一旦处理完所有返回的行,rs.next()将返回一个 false,表示没有要处理的行了。

在 while 循环中,处理每个返回的行。对 ResultSet 对象进行解析,以获得每次传递的给定列名的值。请注意,如果希望列返回一个字符串,则必须调用 ResultSet getString()方法,以字符串格式传递列名。类似地,如果希望该列返回一个 int,您可以调用 ResultSet getInt()方法,以字符串格式传递列名。其他数据类型也是如此。这些方法将返回相应的列值。在这个配方的解决方案的例子中,这些值被存储到局部变量中。

String recipe = rs.getString("RECIPE_NUM");
String name = rs.getString("NAME");
String desc = rs.getString("DESCRIPTION");

一旦获得了列值,您就可以对存储在局部变量中的值做您想做的事情。在这种情况下,它们是使用 System.out()方法打印出来的。

System.out.println(recipe + "\t" + name + "\t" + desc);

尝试查询数据库时可能会引发 java.sql.SQLException(例如,如果没有正确获取连接对象,或者如果您尝试查询的数据库表不存在)。在这些情况下,您必须提供异常处理来处理错误。因此,所有数据库处理代码都应该放在 try 块中。catch 块然后处理一个 SQLException,因此如果抛出一个,将使用 catch 块中的代码处理该异常。听起来很简单,对吧?是的,但是每次执行数据库查询时都必须这样做。很多样板代码。

如果语句和连接是打开的,关闭它们总是一个好主意。使用 try-with-resources 构造是最有效的资源管理解决方案。完成后关闭资源将有助于确保系统可以根据需要重新分配资源,并尊重数据库。尽快关闭连接以便其他进程可以使用它们是很重要的。

13-4.执行 CRUD 操作

问题

您需要能够在应用中执行标准的数据库操作。也就是说,您需要创建、检索、更新和删除(CRUD)数据库记录的能力。

解决办法

使用配方 13-1 中提供的解决方案之一创建一个连接对象并获得一个数据库连接;然后使用从 java.sql.Connection 对象获得的 java.sql.Statement 对象执行 CRUD 操作。将用于这些操作的数据库表具有以下格式:

RECIPES (
    id              int not null,
    recipe_number   varchar(10) not null,
    recipe_name     varchar(100) not null,
    description     varchar(500),
    text            clob,
    constraint recipes_pk primary key (id) enable
);

以下代码摘录演示了如何使用 JDBC 执行每个 CRUD 操作:

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;

public class CrudOperations {

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

            createConn = new CreateConnection();
            performCreate();
            performRead();
            performUpdate();
            performDelete();
            System.out.println("-- Final State --");
            performRead();

    }

    private static void performCreate(){
        String sql = "INSERT INTO RECIPES VALUES(" +
                     "next value for recipes_seq, " +
                     "'13-4', " +
                     "'Performing CRUD Operations', " +
                     "'How to perform create, read, update, delete functions', " +
                     "'Recipe Text')";

        try (Connection conn = createConn.getConnection();
                Statement stmt = conn.createStatement();) {
            // Returns row-count or 0 if not successful
            int result = stmt.executeUpdate(sql);
            if (result == 1{
                System.out.println("-- Record created --");
            } else {
                System.err.println("!! Record NOT Created !!");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

    }

    private static void performRead(){
        String qry = "select recipe_number, recipe_name, description from recipes";

        try (Connection conn = createConn.getConnection();
                Statement stmt = conn.createStatement();) {
            ResultSet rs = stmt.executeQuery(qry);
            while (rs.next()) {
                String recipe = rs.getString("RECIPE_NUMBER");
                String name = rs.getString("RECIPE_NAME");
                String desc = rs.getString("DESCRIPTION");

                System.out.println(recipe + "\t" + name + "\t" + desc);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

    }

    private static void performUpdate(){
        String sql = "UPDATE RECIPES " +
                     "SET RECIPE_NUMBER = '13-5' " +
                     "WHERE RECIPE_NUMBER = '13-4'";

        try (Connection conn = createConn.getConnection();
                Statement stmt = conn.createStatement();) {
            int result = stmt.executeUpdate(sql);
            if (result > 0){
                System.out.println("-- Record Updated --");
            } else {
                System.out.println("!! Record NOT Updated !!");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

    }

    private static void performDelete(){
        String sql = "DELETE FROM RECIPES WHERE RECIPE_NUMBER = '13-5'";

        try (Connection conn = createConn.getConnection();
                Statement stmt = conn.createStatement();) {
            int result = stmt.executeUpdate(sql);
            if (result > 0){
                System.out.println("-- Record Deleted --");
            } else {
                System.out.println("!! Record NOT Deleted!!");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

}

下面是运行代码的结果:

Successfully connected
-- Record created --
13-1    Connecting to a Database―DriverManager and DataSource Implementations
13-2    Querying a Database and Retrieving Results      Obtaining and Using Data from a DBMS
13-3    Handling SQL Exceptions Using SQLException
13-4    Performing CRUD Operations      How to Perform Create, Read, Update, Delete Functions
-- Record Updated --
-- Record Deleted --
-- Final State --
13-1    Connecting to a Database        DriverManager and DataSource Implementations
13-2    Querying a Database and Retrieving Results       Obtaining and Using Data from a DBMS
13-3    Handling SQL Exceptions Using SQLException

它是如何工作的

几乎每个数据库任务都使用相同的基本代码格式。格式如下:

  1. 获取到数据库的连接。

  2. 根据连接创建一个语句。

  3. 使用语句执行数据库任务。

  4. 对数据库任务的结果做一些事情。

  5. 关闭语句(如果使用完了,还要关闭数据库连接)。

使用 JDBC 执行查询和使用数据操作语言(DML)执行查询的主要区别在于,根据要执行的操作,您将对语句对象调用不同的方法。要执行查询,需要调用语句 executeQuery()方法。为了执行插入、更新和删除等 DML 任务,请调用 executeUpdate()方法。

这个配方的解决方案中的 performCreate()方法演示了将记录插入数据库的操作。要在数据库中插入记录,请构造一个字符串格式的 SQL INSERT 语句。要执行插入,请将 SQL 字符串传递给语句对象的 executeUpdate()方法。如果执行插入,将返回一个 int 值,指定已插入的行数。如果插入操作未成功执行,将返回零或引发 SQLException,表明语句或数据库连接有问题。

这个配方的解决方案中的 performRead()方法演示了查询数据库的操作。要执行查询,请调用语句对象的 executeQuery()方法,以字符串格式传递 SQL 语句。结果将是一个 ResultSet 对象,然后可以用它来处理返回的数据。有关执行查询的更多信息,请参见配方 13-3。

这个配方的解决方案中的 performUpdate()方法演示了在数据库表中更新记录的操作。首先,构造一个字符串格式的 SQL UPDATE 语句。接下来,为了执行更新操作,将 SQL 字符串传递给语句对象的 executeUpdate()方法。如果更新成功执行,将返回一个 int 值,该值指定更新的记录数。如果更新操作没有成功执行,将返回零或引发 SQLException,表明语句或数据库连接有问题。

需要介绍的最后一个数据库操作是删除操作。这个配方的解决方案中的 performDelete()方法演示了从数据库中删除记录的操作。首先,构造一个字符串格式的 SQL DELETE 语句。接下来,为了执行删除,将 SQL 字符串传递给语句对象的 executeUpdate()方法。如果删除成功,将返回一个指定删除行数的 int 值。否则,如果删除失败,将返回零或引发 SQLException,表明语句或数据库连接有问题。

几乎每个数据库应用都会在某个时候使用至少一个 CRUD 操作。如果您在 Java 应用中使用数据库,这是需要知道的基本 JDBC。即使您不会直接使用 JDBC API,了解这些基础知识也是有好处的。

13-5.简化连接管理

问题

您的应用需要使用数据库,为了使用数据库,您需要为每个交互打开一个连接。您不需要在每次需要访问数据库时编写逻辑来打开数据库连接,而是希望使用单个类来执行该任务。

解决办法

编写一个类来处理应用中的所有连接管理。这样做将允许您调用该类来获得连接,而不是在每次需要访问数据库时设置一个新的连接对象。执行以下步骤为您的 JDBC 应用设置连接管理环境:

  1. 创建一个名为 CreateConnection.java 的类,它将封装应用的所有连接逻辑。

  2. 创建一个属性文件来存储连接信息。将该文件放在类路径中的某个位置,以便 CreateConnection 类可以加载它。

  3. 使用 CreateConnection 类获取数据库连接。

以下代码列出了可用于集中式连接管理的 CreateConnection 类:

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

public class CreateConnection {

    static Properties props = new Properties();

    String hostname = null;
    String port = null;
    String database = null;
    String username = null;
    String password = null;
    String driver = null;
    String jndi = null;

    public CreateConnection() {
        // Looks for properties file in the root of the src directory in Netbeans project
        try (InputStream in = Files.newInputStream(FileSystems.getDefault().
                getPath(System.getProperty("user.dir") + File.separator + "db_props.properties"));) {
            props.load(in);
            in.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        loadProperties();
    }

    public final void loadProperties() {
        hostname = props.getProperty("host_name");
        port = props.getProperty("port_number");
        database = props.getProperty("db_name");
        username = props.getProperty("username");
        password = props.getProperty("password");
        driver = props.getProperty("driver");
        jndi = props.getProperty("jndi");

    }

    /**
     * Demonstrates obtaining a connection via DriverManager
     *
     * @return
     * @throws SQLException
     */
    public Connection getConnection() throws SQLException {
        Connection conn = null;
        String jdbcUrl;
        if (driver.equals("derby")) {
            jdbcUrl = "jdbc:derby://" + this.hostname + ":"
                    + this.port + "/" + this.database;
        } else {
            jdbcUrl = "jdbc:oracle:thin:@" + this.hostname + ":"
                    + this.port + ":" + this.database;
        }
        conn = DriverManager.getConnection(jdbcUrl, username, password);
        System.out.println("Successfully connected");
        return conn;
    }

    /**
     * Demonstrates obtaining a connection via a DataSource object
     *
     * @return
     */
    public Connection getDSConnection() {
        Connection conn = null;
        try {
            Context ctx = new InitialContext();
            DataSource ds = (DataSource) ctx.lookup(this.jndi);
            conn = ds.getConnection();
        } catch (NamingException | SQLException ex) {
            ex.printStackTrace();
        }
        return conn;
    }
}

接下来,下面几行文本是属性文件中应该包含的内容的示例,该属性文件用于获取到数据库的连接。对于本例,属性文件被命名为 db_props.properties:

host_name=your_db_server_name
db_name=your_db_name
username=db_username
password=db_username_password
port_number=db_port_number
#driver = derby or oracle
driver=db_driver
jndi=jndi_connection_String

最后,使用 CreateConnection 类获取应用的连接。下面的代码演示了这个概念:

CreateConnection createConn = new CreateConnection();
try(Connection conn = createConn.getConnection()) {
    performDbTask();
} catch (java.sql.SQLException ex) {
    ex.printStackTrace();
}

这段代码使用 try-with-resources 在完成数据库任务后自动关闭连接。

它是如何工作的

在数据库应用中获取连接可能需要大量代码。此外,如果每次需要获得连接时都要重新键入代码,这个过程很容易出错。通过将数据库连接逻辑封装在单个类中,您可以在每次需要连接到数据库时重用相同的连接代码。这提高了您的工作效率,减少了输入错误的机会,也增强了可管理性,因为如果您必须进行更改,它可以在一个地方而不是在几个不同的位置发生。

创建一个战略连接方法对你和其他将来可能需要维护你的代码的人是有益的。虽然在使用应用服务器或 JNDI 时,数据源是管理数据库连接的首选技术,但是这个方法的解决方案演示了使用标准的 JDBC 驱动程序管理器连接。使用 DriverManager 的一个安全问题是,您需要将数据库凭证存储在某个地方,供应用使用。将这些凭证以纯文本的形式存储在任何地方都是不安全的,将它们嵌入到应用代码中也是不安全的,因为应用代码将来可能会被反编译。如解决方案所示,磁盘上的属性文件用于存储数据库凭证。假设这个属性文件在部署到服务器之前会被加密,并且应用能够处理解密。

如解决方案所示,代码从属性文件中读取数据库凭证、主机名、数据库名和端口号。然后将这些信息拼凑起来形成一个 JDBC URL,DriverManager 可以使用它来获得到数据库的连接。一旦获得,该连接可以在任何地方使用,然后关闭。类似地,如果使用已经部署到应用服务器的数据源,属性文件可以用来存储 JNDI 连接。这是使用数据源连接到数据库所需的唯一信息。对于使用 connection 类的开发人员来说,这两种类型的连接之间的唯一区别是为了获得 Connection 对象而调用的方法名。

您可以开发一个 JDBC 应用,这样用于获得连接的代码就需要从头到尾都是硬编码的。相反,这种解决方案使获取连接的所有代码都被一个类封装起来,这样开发人员就不需要担心了。这种技术还允许代码变得更易于维护。例如,如果应用最初是使用 DriverManager 部署的,但是后来有了使用数据源的能力,那么只需要修改很少的代码。

13-6.防范 SQL 注入

问题

您的应用执行数据库任务。为了减少 SQL 注入攻击的机会,您需要确保没有未经过滤的文本字符串被附加到 SQL 语句中并针对数据库执行。

小费

尽管准备好的语句是解决这一问题的方法,但它们不仅仅可以用来防范 SQL 注入病毒。它们还提供了集中和更好地控制应用中使用的 SQL 的方法。例如,您可以将查询创建一次,作为一个准备好的语句,然后从代码中的许多不同位置调用它,而不是创建同一个查询的多个可能不同的版本。对查询逻辑的任何更改只需要在准备语句时进行。

解决办法

使用 PreparedStatements 执行数据库任务。PreparedStatements 将预编译的 SQL 语句而不是字符串发送到 DBMS。以下代码演示如何使用 java.sql.PreparedStatement 对象执行数据库查询和数据库更新。

在下面的代码示例中,PreparedStatement 用于查询数据库中的给定记录。假设配方编号的字符串[]作为一个变量被传递给这个代码。

private static void queryDbRecipe(String[] recipeNumbers) {
    String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION "
            + "FROM RECIPES "
            + "WHERE RECIPE_NUMBER = ?";

    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
        for (String recipeNumber : recipeNumbers) {
            pstmt.setString(1, recipeNumber);
            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getString(2) + ": " + rs.getString(3)
                        + " - " + rs.getString(4));
            }
        }
    } catch (SQLException ex) {
        ex.printStackTrace();
    }

}

下一个示例演示了如何使用 PreparedStatement 将记录插入数据库。假设 recipeNumber、title、description 和 text 字符串作为变量传递给了这段代码。

String sql = "INSERT INTO RECIPES VALUES(" +
             "NEXT VALUE FOR RECIPES_SEQ, ?,?,?,?)";
try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
    pstmt.setString(1, recipeNumber);
    pstmt.setString(2, title);
    pstmt.setString(3, description);
    pstmt.setString(4, text);
    pstmt.executeUpdate();
    System.out.println("Record successfully inserted.");
} catch (SQLException ex){
    ex.printStackTrace();
}

在最后一个示例中,PreparedStatement 用于从数据库中删除记录。同样,假设 recipeNumber 字符串作为变量传递给这段代码。

String sql = "DELETE FROM RECIPES WHERE " +
             "RECIPE_NUMBER = ?";
try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
    pstmt.setString(1, recipeNumber);
    pstmt.executeUpdate();
    System.out.println("Recipe " + recipeNumber + " successfully deleted.");
} catch (SQLException ex){
    ex.printStackTrace();
}

如您所见,PreparedStatement 与标准的 JDBC 语句对象非常相似,但它将预编译的 SQL 而不是文本字符串发送到 DBMS。

它是如何工作的

虽然标准的 JDBC 语句可以完成工作,但残酷的现实是,它们有时不安全,使用起来很麻烦。例如,如果使用动态 SQL 语句来查询数据库,并且将用户接受的字符串赋给变量并与预期的 SQL 字符串连接,就会发生不好的事情。在大多数情况下,用户接受的字符串将被连接起来,SQL 字符串将按预期用于查询数据库。然而,攻击者可以决定将恶意代码放入字符串中(也称为 SQL 注入),然后使用标准语句对象将恶意代码无意中发送到数据库。使用 PreparedStatements 可以防止此类恶意字符串连接成 SQL 字符串并传递到 DBMS,因为它们使用不同的方法。PreparedStatements 使用替代变量而不是串联来使 SQL 字符串动态化。它们也是预编译的,这意味着在 SQL 被发送到 DBMS 之前就形成了有效的 SQL 字符串。此外,PreparedStatements 可以帮助您的应用更好地执行,因为如果同一个 SQL 必须运行多次,它只需编译一次。之后,替代变量是可互换的,但是整个 SQL 可以由 PreparedStatement 非常快速地执行。

让我们看看 PreparedStatement 在实践中是如何工作的。如果您查看这个配方的解决方案中的第一个示例,您可以看到数据库表 RECIPES 正在被查询,传递一个 RECIPE_NUMBER 并检索匹配记录的结果。SQL 字符串如下所示:

String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION " +
             "FROM RECIPES " +
             "WHERE RECIPE_NUM = ?";

除了问号(?)在字符串的末尾。在 SQL 字符串中放置一个问号表示在执行 SQL 时将使用一个替代变量来代替这个问号。使用 PreparedStatement 的下一步是声明 PreparedStatement 类型的变量。这可以从下面一行代码中看出:

PreparedStatement pstmt = null;

PreparedStatement 实现 AutoCloseable,因此可以在 try-with-resources 块的上下文中使用它。一旦声明了 PreparedStatement,就可以使用它了。但是,使用 PreparedStatement 可能不会导致引发异常。因此,在不使用 try-with-resources 的情况下,应该在 try-catch 块中出现 PreparedStatement,以便可以优雅地处理任何异常。例如,如果数据库连接由于某种原因不可用,或者 SQL 字符串无效,就会出现异常。最好在 catch 块中明智地处理异常,而不是因为这些问题而导致应用崩溃。下面的 try-catch 块包含将 SQL 字符串发送到数据库并检索结果所需的代码:

try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
    pstmt.setString(1, recipeNumber);
    ResultSet rs = pstmt.executeQuery();
    while(rs.next()){
        System.out.println(rs.getString(2) + ": " + rs.getString(3) +
                        " - " + rs.getString(4));
    }
} catch (SQLException ex) {
    ex.printStackTrace();
}

首先,可以看到 Connection 对象用于实例化一个 PreparedStatement 对象。SQL 字符串在创建时被传递给 PreparedStatement 对象的构造函数。由于 PreparedStatement 是在 try-with-resources 构造中实例化的,因此当它不再使用时将自动关闭。接下来,PreparedStatement 对象用于为已经放入 SQL 字符串中的任何替代变量设置值。如您所见,示例中使用了 PreparedStatement setString()方法将位置 1 处的替换变量设置为 recipeNumber 变量的内容。替代变量的位置与问号(?)放在 SQL 字符串中。字符串中的第一个问号被分配给第一个位置,第二个问号被分配给第二个位置,依此类推。如果要分配多个替代变量,将会有多个针对 PreparedStatement 的调用,分配每个变量,直到每个变量都被考虑在内。PreparedStatements 可以接受许多不同数据类型的替代变量。例如,如果一个 int 值被赋给一个替代变量,调用 setInt(position,variable)方法是合适的。有关可用于使用 PreparedStatement 对象分配替代变量的完整方法集,请参见联机文档或 IDE 的代码完成。

一旦所有变量都被赋值,就可以执行 SQL 字符串了。PreparedStatement 对象包含一个 executeQuery()方法,该方法用于执行表示查询的 SQL 字符串。executeQuery()方法返回一个 ResultSet 对象,该对象包含为特定 SQL 查询从数据库中获取的结果。接下来,可以遍历 ResultSet 以获取从数据库中检索的值。同样,通过调用 ResultSet 对象的相应 getter 方法并传递您想要获取的列值的位置,位置赋值用于检索结果。位置由列名在 SQL 字符串中出现的顺序决定。在该示例中,第一个位置对应于 RECIPE_NUMBER 列,第二个位置对应于 RECIPE_NAME 列,依此类推。如果 recipeNumber 字符串变量等于“13-1”,则在示例中执行查询的结果将如下所示:

13-1: Connecting to a Database - DriverManager and DataSource Implementations

当然,如果替代变量设置不正确或者 SQL 字符串有问题,就会抛出异常。这将导致包含在 catch 块中的代码被执行。您还应该确保在使用 PreparedStatements 后进行清理,方法是在使用完语句后关闭该语句。如果没有使用 try-with-resources 构造,最好将所有清理代码放在 finally 块中,以确保即使抛出异常,PreparedStatement 也能正确关闭。在该示例中,finally 块如下所示:

finally {
    if (pstmt != null){
        try {
            pstmt.close();
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
    }
}

可以看到,已实例化的 PreparedStatement 对象 pstmt 被检查是否为 NULL。如果没有,则通过调用 close()方法关闭它。

通过研究这个配方的解决方案中的代码,您可以看到类似的代码用于处理数据库插入、更新和删除语句。这两种情况的唯一区别是调用了 PreparedStatement executeUpdate()方法,而不是 executeQuery()方法。executeUpdate()方法将返回一个 int 值,表示受 SQL 语句影响的行数。

PreparedStatement 对象的使用优于 JDBC 语句对象。这是因为它们更安全,性能更好。它们还可以使您的代码更容易遵循和维护。

13-7.执行交易

问题

构建应用的方式需要任务的顺序处理。一个任务依赖于另一个任务,每个进程执行不同的数据库操作。如果其中一个任务失败,已经发生的数据库处理需要被逆转。

解决办法

将连接对象自动提交设置为 false,然后执行要完成的事务。一旦成功地执行了每个事务,手动提交连接对象;否则,回滚已发生的每个事务。下面的代码示例演示事务管理。如果您查看 TransactionExample 类的 main()方法,您将看到 Connection 对象的 autoCommit()首选项已被设置为 false,因此数据库语句被组合在一起形成一个事务。如果事务内的所有语句都成功,则通过调用 commit()方法手动提交连接对象;否则,通过调用 rollback()方法回滚所有语句。默认情况下,autoCommit 设置为 true,这将自动将每个语句视为单个事务。

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;

public class TransactionExample {
    public static Connection conn = null;

    public static void main(String[] args) {
        boolean successFlag = false;
        try {
            CreateConnection createConn = new CreateConnection();
            conn = createConn.getConnection();
            conn.setAutoCommit(false);
            queryDbRecipes();
            successFlag = insertRecord(
                    "13-6",
                    "Simplifying and Adding Security with Prepared Statements",
                    "Working with Prepared Statements",
                    "Recipe Text");

            if (successFlag == true){

                successFlag = insertRecord(
                        "13-6B",
                        "Simplifying and Adding Security with Prepared Statements",
                        "Working with Prepared Statements",
                        "Recipe Text");
            }

            // Commit Transactions
            if (successFlag == true)
                conn.commit();  
            else
                conn.rollback();

            conn.setAutoCommit(true);
            queryDbRecipes();
        } catch (java.sql.SQLException ex) {
            System.out.println(ex);
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        }

    }

    private static void queryDbRecipes(){
        String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION " +
                     "FROM RECIPES";

        try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
            ResultSet rs = pstmt.executeQuery();
            while(rs.next()){
                System.out.println(rs.getString(2) + ": " + rs.getString(3) +
                                " - " + rs.getString(4));
            }
        } catch (SQLException ex) {
            ex.printStackTrace();
        }

    }

    private static boolean insertRecord(String recipeNumber,
                              String title,
                              String description,
                              String text){
        String sql = "INSERT INTO RECIPES VALUES(" +
                     "NEXT VALUE FOR RECIPES_SEQ, ?,?,?,?)";
        boolean success = false;
        try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
            pstmt.setString(1, recipeNumber);
            pstmt.setString(2, title);
            pstmt.setString(3, description);
            pstmt.setString(4, text);
            pstmt.executeUpdate();
            System.out.println("Record successfully inserted.");
            success = true;
        } catch (SQLException ex){
            success = false;
            ex.printStackTrace();
        }
        return success;

    }

}

最后,如果任何语句失败,所有事务都将回滚。但是,如果所有语句都正确执行,所有内容都将被提交。

它是如何工作的

事务管理在应用中扮演着重要的角色。对于执行相互依赖的不同任务的应用来说尤其如此。在许多情况下,如果在一个事务中执行的任务之一失败,那么整个事务失败要比只完成一部分要好。例如,假设您正在向应用数据库添加数据库用户记录。现在,假设为您的应用添加一个用户需要修改几个不同的数据库表,可能是一个角色表,等等。如果第一个表修改正确,第二个表修改失败,会发生什么?您将得到一个部分完成的应用用户添加,并且您的用户很可能无法像预期的那样访问应用。在这种情况下,如果其中一个更新失败,最好回滚所有已经完成的数据库修改,这样数据库就处于干净的状态,可以再次尝试事务。

默认情况下,会设置一个连接对象,以便打开自动提交。这意味着每个数据库插入、更新或删除语句都会被立即提交。通常,这是您希望应用运行的方式。但是,在您有许多相互依赖的数据库语句的情况下,关闭自动提交以便可以一次提交所有语句是很重要的。为此,调用连接对象的 setAutoCommit()方法并传递一个 false 值。正如您在这个配方的解决方案中所看到的,setAutoCommit()方法被称为传递假值,数据库语句被执行。这样做将导致所有数据库语句更改都是临时的,直到调用连接对象的 commit()方法。这为您提供了在发出 commit()之前确保所有语句正确执行的能力。看一下这个配方的解决方案中 TransactionExample 类的 main()方法中包含的事务管理代码:

boolean successFlag = false;
...
CreateConnection createConn = new CreateConnection();
conn = createConn.getConnection();
conn.setAutoCommit(false);
queryDbRecipes();
successFlag = insertRecord(
                    "13-6",
                    "Simplifying and Adding Security with Prepared Statements",
                    "Working with Prepared Statements",
                    "Recipe Text");

if (successFlag == true){

    successFlag = insertRecord(
        null,
        "Simplifying and Adding Security with Prepared Statements",
        "Working with Prepared Statements",
        "Recipe Text");
}
// Commit Transactions
if (successFlag == true)
    conn.commit();  
else
     conn.rollback();

conn.setAutoCommit(true);

请注意,只有在成功处理了所有事务语句的情况下,才会调用 commit()方法。如果其中任何一个失败,successFlag 等于 false,这将导致调用 rollback()方法。在这个配方的解决方案中,对 insertRecord()的第二次调用试图向配方中插入一个空值。ID 列,这是不允许的。因此,该插入会失败,所有内容(包括前一次插入)都会回滚。

13-8.创建可滚动的结果集

问题

您已经查询了数据库并获得了一些结果。您希望将这些结果存储在一个对象中,该对象将允许您在结果中向前和向后遍历,并根据需要更新值。

解决办法

创建一个可滚动的 ResultSet 对象,然后您将能够读取下一条、第一条记录、最后一条和上一条记录。使用可滚动的 ResultSet 允许从任何方向获取查询结果,以便可以根据需要检索数据。下面的示例方法演示了如何创建可滚动的 ResultSet 对象:

private static void queryDbRecipes(){
    String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION " +
                 "FROM RECIPES";

    try(PreparedStatement pstmt =conn.prepareStatement(sql,
            ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
        ResultSet rs = pstmt.executeQuery()) {

        rs.first();
        System.out.println(rs.getString(2) + ": " + rs.getString(3) +
                        " - " + rs.getString(4));
        rs.next();
        System.out.println(rs.getString(2) + ": " + rs.getString(3) +
                        " - " + rs.getString(4));
        rs.previous();
        System.out.println(rs.getString(2) + ": " + rs.getString(3) +
                        " - " + rs.getString(4));
        rs.last();
        System.out.println(rs.getString(2) + ": " + rs.getString(3) +
                        " - " + rs.getString(4));
    } catch (SQLException ex) {
        ex.printStackTrace();
    }

}

使用最初为本章加载的数据,执行此方法将产生以下输出:

Successfully connected
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to Come
13-2: Querying a Database and Retrieving Results - Obtaining and Using Data from a DBMS
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to Come
13-3: Handling SQL Exceptions - Using SQLException

它是如何工作的

普通的 ResultSet 对象允许向前提取结果。也就是说,应用可以从检索到的第一条记录到最后一条记录处理默认的 ResultSet 对象。有时候,在遍历结果集时,应用需要更多的功能。例如,假设您想编写一个应用,允许某人显示检索到的第一条或最后一条记录,或者在结果中向前或向后翻页。使用一个标准的结果集,你不可能很容易地做到这一点。但是,通过创建一个可滚动的结果集,您可以轻松地在结果中前后移动。

若要创建可滚动的结果集,必须首先创建能够创建可滚动结果集的语句或 PreparedStatement 的实例。也就是说,在创建语句时,必须将 ResultSet 滚动类型常量值传递给 Connection 对象的 createStatement()方法。同样,在使用 PreparedStatement 时,必须将滚动类型常量值传递给连接对象的 prepareStatement()方法。有三种滚动类型常量可供使用。表 13-1 显示了这三个常数。

表 13-1。结果集滚动类型常量
|

常数

|

描述

|
| --- | --- |
| 结果集。仅转发类型 | 默认类型,仅允许向前移动。 |
| 结果集。TYPE _ SCROLL _ 不敏感 | 允许向前和向后移动。对结果集更新不敏感。 |
| 结果集。类型 _ 滚动 _ 敏感 | 允许向前和向后移动。对结果集更新敏感。 |

您还必须传递一个 ResultSet 并发常量,以告知 ResultSet 是否是可更新的。默认值为 ResultSet。CONCUR_READ_ONLY,这意味着结果集不可更新。另一种并发类型是 ResultSet。CONCUR_UPDATABLE,表示可更新的结果集对象。

在该配方的解决方案中,使用了一个 PreparedStatement 对象,创建一个能够生成可滚动结果集的 PreparedStatement 对象的代码如下所示:

pstmt = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE,
                                                           ResultSet.CONCUR_READ_ONLY);

这样创建 PreparedStatement 后,将返回一个可滚动的 ResultSet。您可以使用可滚动的 ResultSet 在多个方向上遍历,方法是调用 ResultSet 方法来指示您想要移动的方向或想要的位置。以下代码行将检索结果集中的第一条记录:

ResultSet rs = pstmt.executeQuery();
rs.first();

这个配方的解决方案演示了几种不同的滚动方向。具体来说,您可以看到调用 ResultSet first()、next()、last()和 previous()方法是为了移动到 ResultSet 中的不同位置。有关 ResultSet 对象的完整参考,请参见位于docs . Oracle . com/javase/8/docs/API/Java/SQL/ResultSet . html的在线文档。

可滚动的 ResultSet 对象在应用开发中有一席之地。当你需要它们的时候,它们是那些美好事物中的一种,但它们也是你可能不经常需要的东西。

13-9.创建可更新的结果集

问题

一个应用任务查询了数据库并获得了结果。您已经将这些结果存储到一个 ResultSet 对象中,并且希望更新 ResultSet 中的一些值,并将它们提交回数据库。

解决办法

使 ResultSet 对象可更新,然后在迭代结果时根据需要更新行。以下示例方法演示了如何使结果集可更新,然后如何更新该结果集中的内容,最终将其保存在数据库中:

private static void queryAndUpdateDbRecipes(String recipeNumber){
        String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION " +
                     "FROM RECIPES " +
                     "WHERE RECIPE_NUMBER = ?";
        ResultSet rs = null;
        try (PreparedStatement pstmt =
                conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);){

            pstmt.setString(1, recipeNumber);
            rs = pstmt.executeQuery();
            while(rs.next()){
                String desc = rs.getString(4);
                System.out.println("Updating row" + desc);

                rs.updateString(4, desc + " -- More to come");
                rs.updateRow();
            }

        } catch (SQLException ex) {
            ex.printStackTrace();
        } finally {
            if (rs != null){
                try {
                    rs.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        }

这个方法可以称为传递包含配方号的字符串值。假设配方号“13-1”被传递给了这个方法;结果将是以下输出:

Successfully connected
13-1: Connecting to a Database - DriverManager and DataSource Implementations  
13-2: Querying a Database and Retrieving Results - Obtaining and Using Data from a DBMS
13-3: Handling SQL Exceptions - Using SQLException
Updating rowDriverManager and DataSource Implementations  
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to come
13-2: Querying a Database and Retrieving Results - Obtaining and Using Data from a DBMS
13-3: Handling SQL Exceptions - Using SQLException

它是如何工作的

有时您需要在解析数据时更新数据。通常这种技术包括测试从数据库返回的值,并在与另一个值比较后更新它们。最简单的方法是通过传递 ResultSet 使 ResultSet 对象可更新。连接对象的 createStatement()或 prepareStatement()方法的 CONCUR_UPDATABLE 常量。这样做会导致语句或 PreparedStatement 生成可更新的结果集。

注意

一些数据库 JDBC 驱动程序不支持可更新的结果集。有关详细信息,请参阅您的 JDBC 驱动程序文档。这段代码是在 Oracle 数据库 11.2 版上使用 Oracle 的 ojdbc6.jar JDBC 驱动程序运行的。

创建将产生可更新结果集的语句的格式是将结果集类型作为第一个参数传递,将结果集并发性作为第二个参数传递。滚动类型必须是 TYPE_SCROLL_SENSITIVE,以确保结果集对所做的任何更新敏感。下面的代码通过创建一个语句对象来演示这种技术,该对象将产生一个可滚动和可更新的 ResultSet 对象:

Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);

创建将生成可更新结果集的 PreparedStatement 的格式是将 SQL 字符串作为第一个参数传递,将结果集类型作为第二个参数传递,将结果集并发性作为第三个参数传递。该配方的解决方案使用以下代码行演示了这种技术:

pstmt = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);

本节中讨论的两行代码都将产生可滚动和可更新的 ResultSet 对象。一旦获得了可更新的 ResultSet,就可以像使用普通的 ResultSet 一样获取从数据库中检索到的值。此外,您可以调用 ResultSet 对象的 updateXXX()方法之一来更新 ResultSet 中的任何值。在这个配方的解决方案中,调用了 updateString()方法,将查询值的位置作为第一个参数传递,将更新后的文本作为第二个参数传递。在这种情况下,SQL 查询中列出的第四个元素列将被更新。

rs.updateString(4, desc + " -- More to come");

最后,要持久化您已经更改的值,调用 ResultSet updateRow()方法,如这个配方的解决方案所示:

rs.updateRow();

创建可更新的结果集并不是您每天都需要做的事情。事实上,您可能永远不需要创建可更新的结果集。然而,对于需要这种策略的情况,这种技术会非常方便。

13-10.缓存数据以便在断开连接时使用

问题

当处于断开状态时,您希望使用 DBMS 中的数据。也就是说,您正在一台没有连接到数据库的设备上工作,并且您仍然希望能够像连接到数据库一样处理一组数据。例如,您正在便携式设备上处理数据,并且您不在办公室,没有连接。您希望能够查询、插入、更新和删除数据,即使没有连接。一旦连接可用,您希望让您的设备同步断开连接时所做的任何数据库更改。

解决办法

使用 CachedRowSet 对象存储要在脱机时使用的数据。这将使您的应用能够像连接到数据库一样处理数据。连接恢复或连接回数据库后,将 CachedRowSet 中已更改的数据与数据库存储库同步。下面的示例类演示 CachedRowSet 的用法。在这种情况下,main()方法执行示例。但是,假设没有 main()方法,便携设备上的另一个应用将调用该类的方法。遵循示例中的代码,并考虑在未连接到数据库的情况下使用存储在 CachedRowSet 中的结果的可能性。例如,假设您在连接到网络的情况下开始在办公室工作,而现在在办公室之外,网络不稳定,您无法保持与数据库的持续连接:

package org.java9recipes.chapter13.recipe13_10;

import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.rowset.CachedRowSet;
import javax.sql.rowset.RowSetFactory;
import javax.sql.rowset.RowSetProvider;
import javax.sql.rowset.spi.SyncProviderException;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;

public class CachedRowSetExample {

    public static Connection conn = null;
    public static CreateConnection createConn;
    public static CachedRowSet crs = null;

    public static void main(String[] args) {
        boolean successFlag = false;
        try {
            createConn = new CreateConnection();
            conn = createConn.getConnection();
            // Perform Scrollable Query
            queryWithRowSet();

            // Update the CachedRowSet
            updateData();

            // Synchronize changes
            syncWithDatabase();
        } catch (java.sql.SQLException ex) {
            System.out.println(ex);
        } finally {

            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        }

    }

    /**
     * Call this method to synchronize the data that has been used in the
     * CachedRowSet with the database
     */
    public static void syncWithDatabase() {
        try {
            crs.acceptChanges(conn);
        } catch (SyncProviderException ex) {
            // If there is a conflict while synchronizing, this exception
            // will be thrown.
            ex.printStackTrace();
        } finally {
            // Clean up resources by closing CachedRowSet
            if (crs != null) {
                try {
                    crs.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    public static void queryWithRowSet() {
        RowSetFactory factory;

        try  {

            // Create a new RowSetFactory
            factory = RowSetProvider.newFactory();

            // Create a CachedRowSet object using the factory
            crs = factory.createCachedRowSet();

            // Alternatively populate the CachedRowSet connection settings
            // crs.setUsername(createConn.getUsername());
            // crs.setPassword(createConn.getPassword());
            // crs.setUrl(createConn.getJdbcUrl());

            // Populate a query that will obtain the data that will be used
            crs.setCommand("select id, recipe_number, recipe_name, description from recipes");
            // Set key columns
            int[] keys = {1};
            crs.setKeyColumns(keys);
            crs.execute(conn);

            // You can now work with the object contents in a disconnected state
            while (crs.next()) {
                System.out.println(crs.getString(2) + ": " + crs.getString(3)
                        + " - " + crs.getString(4));
            }

        } catch (SQLException ex) {
            ex.printStackTrace();
        }
    }

    public static boolean updateData() {
        boolean returnValue = false;
        try {
            // Move to the position before the first row in the result set
            crs.beforeFirst();

            // traverse result set
            while (crs.next()) {
                // If the recipe_num equals 11-2 then update
                if (crs.getString("RECIPE_NUMBER").equals("13-2")) {
                    System.out.println("updating recipe 13-2");
                    crs.updateString("description", "Subject to change");
                    crs.updateRow();
                }

            }
          returnValue = true;

          // Move to the position before the first row in the result set
            crs.beforeFirst();

            // traverse result set to see changes
            while (crs.next()) {

                    System.out.println(crs.getString(2) + ": " + crs.getString(3)
                        + " - " + crs.getString(4));

            }

        } catch (SQLException ex) {
            returnValue = false;
            ex.printStackTrace();
        }
        return returnValue;
    }
}

运行此示例代码将显示类似于以下代码的输出,尽管文本可能会因数据库中的值而异。请注意,在更新 CachedRowSet 后,配方 13-2 的数据库记录有一个更改的描述。

Successfully connected
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to Come
13-2: Querying a Database and Retrieving Results - Subject to Change
13-3: Handling SQL Exceptions - Using SQLException
Updating Recipe 13-2
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to Come
13-2: Querying a Database and Retrieving Results - Obtaining and Using Data from a DBMS
13-3: Handling SQL Exceptions - Using SQLException

它是如何工作的

如果您在移动设备上工作或旅行,不可能一直保持与互联网的连接。现在有一些设备可以让你在旅途中完成大量的工作,即使你没有直接连接到数据库。在这种情况下,像 CachedRowSet 对象这样的解决方案就可以发挥作用了。CachedRowSet 与常规的 ResultSet 对象相同,只是它不必为了保持可用而保持与数据库的连接。可以查询数据库,获取结果,放入 CachedRowSet 对象中;然后在没有连接到数据库的情况下使用它们。如果在任何时候对数据进行了更改,这些更改可以在以后与数据库同步。

有几种方法可以创建 CachedRowSet。这个配方的解决方案使用 RowSetFactory 来实例化 CachedRowSet。但是,您也可以使用 CachedRowSet 默认构造函数来创建新的实例。这样做将类似于下面的代码行:

CachedRowSet crs = new CachedRowSetImpl();

一旦实例化,您需要建立到数据库的连接。还有几种方法可以做到这一点。可以为将要使用的连接设置属性,这个方法的解决方案在注释中演示了这种技术。以下解决方案摘录使用 CachedRowSet 对象的 setUsername()、setPassword()和 setUrl()方法设置连接属性。它们每个都接受一个字符串值,在本例中,该字符串是从 CreateConnection 类获得的:

// Alternatively populate the CachedRowSet connection settings
// crs.setUsername(createConn.getUsername());
// crs.setPassword(createConn.getPassword());
// crs.setUrl(createConn.getJdbcUrl());

建立连接的另一种方法是等待查询执行完毕,并将连接对象传递给 executeQuery()方法。这是在解决这个配方时使用的技术。但是在执行查询之前,必须使用 setCommand()方法设置它,该方法接受一个字符串值。在这种情况下,字符串是您需要执行的 SQL 查询:

crs.setCommand("select id, recipe_number, recipe_name, description from recipes");

接下来,如果 CachedRowSet 将用于更新,则应使用 setKeys()方法记录主键值。该方法接受一个包含键列位置索引的 int 数组。这些键用于标识唯一的列。在这种情况下,查询中列出的第一列 ID 是主键:

int[] keys = {1};
crs.setKeyColumns(keys);

最后,执行查询并使用 execute()方法填充 CachedRowSet。如前所述,execute()方法可选地接受一个连接对象,这允许 CachedRowSet 获得一个数据库连接。

crs.execute(conn);

一旦执行了查询并填充了 CachedRowSet,就可以像使用任何其他结果集一样使用它。您可以使用它向前和向后获取记录,或者通过指定您想要检索的行的绝对位置来获取记录。该配方的解决方案仅演示了其中的几种获取方法,但最常用的方法在表 13-2 中列出。

表 13-2。CachedRowSet 提取方法
|

方法

|

描述

|
| --- | --- |
| 首先() | 移动到集合中的第一行。 |
| beforeFirst() | 移动到集合中第一行之前的位置。 |
| 最后一次 | 移动到集合中最后一行之后的位置。 |
| 下一个() | 移动到集合中的下一个位置。 |
| 最后() | 移动到集合的最后一个位置。 |

可以在 CachedRowSet 中插入和更新行。若要插入行,请使用 moveToInsertRow()方法移动到新的行位置。然后使用与您在行中填充的列的数据类型相对应的各种方法[CachedRowSet、updateString()、updateInt()等]来填充行。一旦在行中填充了每个必需的列,就调用 insertRow()方法,然后调用 moveToCurrentRow()方法。以下代码行演示了如何将记录插入到 RECIPES 表中:

crs.moveToInsertRow();
crs.updateInt(1, sequenceValue); // obtain current sequence values with a prior query
crs.updateString(2, “13-x”);
crs.updateString(3, “This is a new recipe title”);
crs.insertRow();
crs.moveToCurrentRow();

更新行类似于使用可更新的结果集。只需使用 CachedRowSet 对象的方法[updateString()、updateInt()等]来更新值,这些方法对应于您在行内要更新的列的数据类型。一旦更新了行中的一列或多列,就调用 updateRow()方法。这种技术在这个配方的解决方案中得到了演示。

crs.updateString("description", "Subject to change");
crs.updateRow();

若要将任何更新或插入传播到数据库,必须调用 acceptChanges()方法。该方法可以接受一个可选的连接参数,以便连接到数据库。一旦被调用,所有的更改都会被刷新到数据库中。不幸的是,由于自上次为 CachedRowSet 检索数据以来可能已经过了一段时间,因此可能会出现冲突。如果出现这种冲突,将引发 SyncProviderException。您可以捕获这些异常,并使用 SyncResolver 对象手动处理冲突。但是,解决冲突超出了本方法的范围,因此要了解更多信息,请参阅在线文档,该文档可以在 http://download . Oracle . com/javase/tutorial/JDBC/basics/cachedrowset . html 找到。

CachedRowSet 对象为处理数据提供了极大的灵活性,尤其是当您使用的设备并不总是连接到数据库时。然而,在您可以简单地使用标准结果集甚至可滚动结果集的情况下,它们也可能是多余的。

13-11.未连接到数据源时联接行集对象

问题

您希望在未连接到数据库的情况下联接两个或多个行集。也许您的应用被加载到一个并不总是连接到数据库的移动设备上。在这种情况下,您正在寻找一个允许您连接两个或更多查询结果的解决方案。

解决办法

使用 JoinRowSet 从两个关系数据库表中获取数据并连接它们。应该将每个要联接的表中的数据提取到一个行集中,然后可以使用 JoinRowSet 根据这些行集中包含的相关元素来联接每个行集对象。例如,假设数据库中有两个相关的表。其中一个表存储作者列表,另一个表包含这些作者撰写的章节列表。这两个表可以使用 SQL 通过主键和外键关系来连接。

注意

主键是数据库表的每个记录中的唯一标识符,外键是两个表之间的引用约束。

但是,应用不会连接到数据库来进行连接查询,因此必须使用 JoinRowSet 来完成。下面的类清单演示了一种可以使用的策略。在这个场景中,数据库表 BOOK_AUTHOR 的设置如下:

BOOK_AUTHOR(
id          int primary key,
last        varchar(30),
first       varchar(30));

author_work(
id              int primary key,
author_id       int not null,
chapter_number  int not null,
chapter_title   varchar(100) not null,
constraint author_work_fk
foreign key(author_id) references book_author(id));

book(
id          int primary key,
title       varchar(150),
image       varchar(150),
description clob);

使用该表的 Java 代码如下:

package org.java9recipes.chapter13.recipe13_11;

import com.sun.rowset.JoinRowSetImpl;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.rowset.CachedRowSet;
import javax.sql.rowset.JoinRowSet;
import javax.sql.rowset.RowSetFactory;
import javax.sql.rowset.RowSetProvider;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;

public class JoinRowSetExample {

    public static Connection conn = null;
    public static CreateConnection createConn;
    public static CachedRowSet bookAuthors = null;
    public static CachedRowSet authorWork = null;
    public static JoinRowSet jrs = null;

    public static void main(String[] args) {
        boolean successFlag = false;
        try {
            createConn = new CreateConnection();
            conn = createConn.getConnection();
            // Perform Scrollable Query
            queryBookAuthor();
            queryAuthorWork();

            joinRowQuery();
        } catch (java.sql.SQLException ex) {
            System.out.println(ex);
        } finally {

            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            if (bookAuthors != null) {
                try {
                    bookAuthors.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            if (authorWork != null) {
                try {
                    authorWork.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            if (jrs != null) {
                try {
                    jrs.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        }

    }

    public static void queryBookAuthor() {
        RowSetFactory factory;

        try {
            // Create a new RowSetFactory
            factory = RowSetProvider.newFactory();

            // Create a CachedRowSet object using the factory
            bookAuthors = factory.createCachedRowSet();

            // Alternatively populate the CachedRowSet connection settings
            // crs.setUsername(createConn.getUsername());
            // crs.setPassword(createConn.getPassword());
            // crs.setUrl(createConn.getJdbcUrl());

            // Populate a query that will obtain the data that will be used
            bookAuthors.setCommand("SELECT ID, LASTNAME, FIRSTNAME FROM BOOK_AUTHOR");

            bookAuthors.execute(conn);

            // You can now work with the object contents in a disconnected state
            while (bookAuthors.next()) {
                System.out.println(bookAuthors.getString(1) + ": " + bookAuthors.getString(2)
                        + ", " + bookAuthors.getString(3));
            }

        } catch (SQLException ex) {
            ex.printStackTrace();
        }
    }

    public static void queryAuthorWork() {
        RowSetFactory factory;

        try {
            // Create a new RowSetFactory
            factory = RowSetProvider.newFactory();

            // Create a CachedRowSet object using the factory
            authorWork = factory.createCachedRowSet();

            // Alternatively populate the CachedRowSet connection settings
            // crs.setUsername(createConn.getUsername());
            // crs.setPassword(createConn.getPassword());
            // crs.setUrl(createConn.getJdbcUrl());

            // Populate a query that will obtain the data that will be used
            authorWork.setCommand("SELECT AW.ID, AUTHOR_ID, B.TITLE FROM AUTHOR_WORK AW, " +
                    "BOOK B " +
                    "WHERE B.ID = AW.BOOK_ID");

            authorWork.execute(conn);

            // You can now work with the object contents in a disconnected state
            while (authorWork.next()) {
                System.out.println(authorWork.getString(1) + ": " + authorWork.getString(2)
                        + " - " + authorWork.getString(3));
            }

        } catch (SQLException ex) {
            ex.printStackTrace();
        }
    }

    public static void joinRowQuery() {
        try {
            // Create JoinRowSet
            jrs = new JoinRowSetImpl();

            // Add RowSet & Corresponding Keys
            jrs.addRowSet(bookAuthors, 1);
            jrs.addRowSet(authorWork, 2);
            // Alternatively use join-column name
            // jrs.addRowSet(authorWork, "AUTHOR_ID");

            // Traverse Results
            while(jrs.next()){
                System.out.println(jrs.getInt("ID") + ": " +
                                   jrs.getString("TITLE") + " - " +
                                   jrs.getString("FIRSTNAME") + " " +
                                   jrs.getString("LASTNAME"));
            }

        } catch (SQLException ex) {
            ex.printStackTrace();
        }

    }
}

运行该类将产生类似如下的输出:

Successfully connected
100: JUNEAU, JOSH
101: DEA, CARL
102: BEATY, MARK
103: GUIME, FREDDY
104: JOHN, OCONNER
105: TESTER, JOE
110: TESTER, JOE
111: OCONNER, JOHN
1: 100 - Java 8 Recipes
2: 100 - Java 7 Recipes
3: 100 - Java EE 7 Recipes
4: 100 - Introducing Java EE 7
5: 103 - Java 7 Recipes
6: 101 - Java 7 Recipes
7: 111 - Java 7 Recipes
8: 102 - Java 7 Recipes
9: 101 - Java FX 2.0 - Introduction by Example
111: Java 7 Recipes - JOHN OCONNER
103: Java 7 Recipes - FREDDY GUIME
102: Java 7 Recipes - MARK BEATY
101: Java FX 2.0 - Introduction by Example - CARL DEA
101: Java 7 Recipes - CARL DEA
100: Introducing Java EE 7 - JOSH JUNEAU
100: Java EE 7 Recipes - JOSH JUNEAU
100: Java 7 Recipes - JOSH JUNEAU
100: Java 8 Recipes - JOSH JUNEAU

它是如何工作的

JoinRowSet 是两个或多个已填充的行集对象的组合。它可用于根据键/值关系联接两个行集对象,就像 SQL 联接查询一样。为了创建 JoinRowSet,必须首先用相关数据填充两个或多个 RowSet 对象,然后可以将它们分别添加到 JoinRowSet 中以创建组合结果。

在这个菜谱的解决方案中,被查询的表被命名为 BOOK_AUTHOR、BOOK 和 AUTHOR_WORK。BOOK_AUTHOR 表包含作者姓名列表,而 AUTHOR_WORK 表包含书籍列表以及相应的 AUTHOR_ID。图书表包含图书细节。按照 main()方法,首先查询 BOOK_AUTHOR 表,并使用 queryBookAuthor()方法将其结果提取到 CachedRowSet 中。有关使用 CachedRowSet 对象的更多详细信息,请参见配方 13-10。

接下来,调用 queryAuthorBook()方法时,用查询 AUTHOR_WORK 和 BOOK 表的结果填充另一个 CachedRowSet。此时,有两个填充的 CacheRowSet 对象,现在可以使用 JoinRowSet 将它们组合起来。为此,每个查询必须包含一个或多个与另一个表相关的列。在这种情况下,BOOK_AUTHOR。ID 列与 AUTHOR_WORK 相关。AUTHOR_ID 列,因此行集对象必须在这些列值上联接。

main()中调用的最后一个方法是 joinRowQuery()。此方法是所有 JoinRowSet 工作发生的地方。首先,通过实例化 JoinRowSetImpl()对象来创建新的 JoinRowSet:

jrs = new JoinRowSetImpl();
注意

使用 JoinRowSetImpl 时,您将收到一个编译时警告,因为它是一个内部的 SUN 专有 API。但是,Oracle 版本是 OracleJoinRowSet,它没有那么通用。

接下来,通过调用其 addRowSet()方法,将两个 CachedRowSet 对象添加到新创建的 JoinRowSet 中。addRowSet()方法接受两个参数。第一个是要添加到 JoinRowSet 的行集对象的名称,第二个是一个 int 值,指示在 CachedRowSet 中的位置,该值包含将用于实现联接的键值。在这个配方的解决方案中,对 addRowSet()的第一次调用传递 bookAuthors CachedRowSet 和数字 1,因为 bookAuthors CachedRowSet 第一个位置的元素对应于 BOOK_AUTHOR。ID 列。对 addRowSet()的第二次调用传递 authorWork CachedRowSet 和编号 2,因为 authorWork CachedRowSet 第二个位置的元素对应于 AUTHOR_WORK。作者 ID 列。

// Add RowSet & Corresponding Keys
jrs.addRowSet(bookAuthors, 1);
jrs.addRowSet(authorWork, 2);
// Alternatively specify the join-column name
jrs.addRowSet(authorWork, "AUTHOR_ID");

JoinRowSet 现在可以用来获取连接的结果,就像它是一个普通的行集一样。当调用 JoinRowSet 的相应方法[getString()、getInt()等]时,传递与要存储的数据对应的数据库列的名称:

while(jrs.next()){
System.out.println(jrs.getInt("ID") + ": " +
                   jrs.getString("TITLE") + " - " +
                   jrs.getString("FIRSTNAME") + " " +
                   jrs.getString("LASTNAME"));
}

虽然 JoinRowSet 不是每天都需要的,但在对两个相关数据集执行操作时,它会很方便。尤其是当应用没有一直连接到数据库时,或者当您试图使用尽可能少的连接对象时。

13-12.筛选行集中的数据

问题

您的应用查询数据库并返回大量的行。缓存结果集中的行数太大,用户无法一次处理。您希望限制可见的行数,以便可以使用从表中查询的不同数据集执行不同的活动。

解决办法

使用 FilteredRowSet 查询数据库并存储内容。FilteredRowSet 可以配置为筛选查询返回的结果,以便仅显示您想要查看的行。在下面的示例中,创建了一个 filter 类,该类稍后将用于筛选数据库查询返回的结果。示例中的过滤器用于根据作者的姓氏限制可见的行数。下面的类包含过滤器的实现:

package org.java9recipes.chapter13.recipe13_12;

import java.sql.SQLException;
import javax.sql.RowSet;
import javax.sql.rowset.Predicate;

public class AuthorFilter implements Predicate {

  private String[] authors;
  private String colName = null;
  private int colNumber = -1;

  public AuthorFilter(String[] authors, String colName) {
    this.authors = authors;
    this.colNumber = -1;
    this.colName = colName;
  }

  public AuthorFilter(String[] authors, int colNumber) {
    this.authors = authors;
    this.colNumber = colNumber;
    this.colName = null;
  }

  @Override
  public boolean evaluate(Object value, String colName) {

    if (colName.equalsIgnoreCase(this.colName)) {
        for (String author : this.authors) {
            if (author.equalsIgnoreCase((String)value)) {
                return true;
            }
        }
    }
    return false;
  }

  @Override
  public boolean evaluate(Object value, int colNumber) {

    if (colNumber == this.colNumber) {
        for (String author : this.authors) {
            if (author.equalsIgnoreCase((String)value)) {
                return true;
            }
        }
    }
    return false;
  }

  @Override
  public boolean evaluate(RowSet rs) {

    if (rs == null)
      return false;

    try {
      for (int i = 0; i < this.authors.length; i++) {

        String authorLast = null;

        if (this.colNumber > 0) {
          authorLast = (String)rs.getObject(this.colNumber);
        } else if (this.colName != null) {
          authorLast = (String)rs.getObject(this.colName);
        } else {
          return false;
        }

        if (authorLast.equalsIgnoreCase(authors[i])) {
          return true;
        }
      }
    } catch (SQLException e) {
      return false;
    }
    return false;
  }

}

FilteredRowSet 使用该筛选器来限制查询的可见结果。正如您将看到的,利用 FilteredRowSet 可以在应用级别以面向对象的方式过滤数据,而不是在 SQL 数据库级别。好处是,您可以实现一系列过滤器,并将它们应用于同一个结果集,返回所需的结果。使用这样的选项消除了执行返回不同数据集的多个数据库查询的需求。

下面的类演示如何实现 FilteredRowSet。main()方法调用一个名为 implement FilteredRowSet()的方法,它包含用于筛选 BOOK_AUTHOR 和 AUTHOR_WORK 表的查询结果的代码,以便只返回来自姓 DEA 和 JUNEAU 的作者的结果:

package org.java9recipes.chapter13.recipe13_12;

import com.sun.rowset.FilteredRowSetImpl;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.RowSet;
import javax.sql.rowset.FilteredRowSet;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;

public class FilteredRowSetExample {

    public static Connection conn = null;
    public static CreateConnection createConn;
    public static FilteredRowSet frs = null;

    public static void main(String[] args) {
        boolean successFlag = false;
        try {
            createConn = new CreateConnection();
            conn = createConn.getConnection();
            implementFilteredRowSet();
        } catch (java.sql.SQLException ex) {
            System.out.println(ex);
        } finally {

            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            if (frs != null) {
                try {
                    frs.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    public static void implementFilteredRowSet() {

        String[] authorArray = {"DEA", "JUNEAU"};

        AuthorFilter authorFilter = new AuthorFilter(authorArray, 2);

        try {
            frs = new FilteredRowSetImpl();

            frs.setCommand("SELECT TITLE, LASTNAME "
                    + "FROM BOOK_AUTHOR BA, "
                    + "     AUTHOR_WORK AW, "
                    + "     BOOK B "
                    + "WHERE AW.AUTHOR_ID = BA.ID "
                    + "AND B.ID = AW.BOOK_ID");

            frs.execute(conn);

            System.out.println("Prior to adding filter:");
            viewRowSet(frs);
            System.out.println("Adding author filter:");
            frs.beforeFirst();
            frs.setFilter(authorFilter);
            viewRowSet(frs);
        } catch (SQLException e) {
            e.printStackTrace();
        }

    }

    public static void viewRowSet(RowSet rs) {
        try {
            while (rs.next()) {
                System.out.println(rs.getString(1) + " - "
                        + rs.getString(2));
            }
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
    }
}

运行这段代码的结果将类似于下面几行。请注意,FilteredRowSet 只返回与筛选器中列出的作者对应的数据行。

Successfully connected
Prior to adding filter:
Java 7 Recipes - JUNEAU
Java 7 Recipes - BEATY
Java 7 Recipes - DEA
Java 7 Recipes - GUIME
Java 7 Recipes - OCONNER
Java EE 7 Recipes - JUNEAU
Java FX 2.0 - Introduction by Example - DEA
Adding author filter:
Java 7 Recipes - JUNEAU
Java 7 Recipes - DEA
Java EE 7 Recipes - JUNEAU
Java FX 2.0 - Introduction by Example – DEA

它是如何工作的

通常,数据库查询返回的结果包含大量的行。正如您可能知道的,太多的行会在可视化处理数据时产生问题。通过在 SQL 语句中使用 WHERE 子句来限制查询返回的行数,以便只返回相关的数据,这通常会有所帮助。但是,如果应用将数据检索到内存中的行集中,然后需要在没有其他数据库请求的情况下根据各种条件筛选数据,则需要使用查询以外的方法。FilteredRowSet 可用于筛选已填充行集中显示的数据,以便更易于管理。

使用 FilteredRowSet 有两个部分。首先,需要创建一个过滤器,用于指定应该如何过滤数据。过滤器类应该实现谓词接口。可能有多个构造函数,每个构造函数接受一组不同的参数,过滤器可能包含多个 evaluate()方法,每个方法接受不同的参数并包含不同的实现。构造函数应该接受可用于筛选行集的内容数组。它们还应该接受第二个参数,或者是筛选器应该针对的列名,或者是筛选器应该针对的列的位置。在这个配方的解决方案中,过滤器类被命名为 AuthorFilter,它用于根据作者姓名数组过滤数据。它的每个构造函数都接受一个数组,该数组包含要过滤的作者姓名,以及列名或位置。每个 evaluate()方法的任务是确定给定的数据行是否与指定的过滤器匹配;在这种情况下,通过数组传递的作者姓名。如果将列名而不是位置传递给过滤器,则调用第一个 evaluate()方法,如果传递了列位置,则调用第二个 evaluate()方法。最后一个 evaluate()方法接受行集本身,它执行遍历数据并返回一个布尔值的工作,以指示相应的列名/位置值是否与筛选数据匹配。

FilteredRowSet 实现的第二部分是 FilteredRowSet 的工作。这可以在 FilteredRowSetExample 类的 implementFilteredRowSet()方法中看到。FilteredRowSet 实际上将使用您编写的 filter 类来确定要显示哪些行。您可以看到,将传递给 filter 类的值数组是该方法中的第一个声明。第二个声明是过滤器类 AuthorFilter 的实例化。当然,过滤器值的数组和对应于过滤器值的列位置被传递到过滤器构造函数中。

String[] authorArray = {"DEA", "JUNEAU"};

// Creates a filter using the array of authors
AuthorFilter authorFilter = new AuthorFilter(authorArray, 2);

若要实例化 FilteredRowSet,请创建 FilteredRowSetImpl 类的新实例。实例化后,只需使用 setCommand()方法设置用于获取结果的 SQL 查询,然后通过调用 executeQuery()方法来执行它。

// Instantiate a new FilteredRowSet
frs = new FilteredRowSetImpl();
// Set the query
frs.setCommand("SELECT TITLE, LASTNAME "
              + "FROM BOOK_AUTHOR BA, "
              + "     AUTHOR_WORK AW, "
              + "     BOOK B "
              + "WHERE AW.AUTHOR_ID = BA.ID "
              + "AND B.ID = AW.BOOK_ID");
// Execute the query
frs.execute(conn);
注意

使用 FilteredRowSetImpl 时,您将收到一个编译时警告,因为它是 Sun Microsystems 生产的一个较旧的内部专有 API。

请注意,过滤器尚未应用。实际上,此时您拥有的是一个可滚动的行集,其中填充了来自查询的所有结果。该示例在应用筛选器之前显示这些结果。若要应用过滤器,请使用 setFilter()方法,将过滤器作为参数传递。完成后,FilteredResultSet 将只显示那些与筛选器指定的条件相匹配的行。

同样,FilteredRowSet 技术也有其用武之地,尤其是当您使用的应用可能不总是连接到数据库时。它是一个强大的工具,可以用来过滤数据、处理数据,然后应用不同的过滤器并处理新的结果。这类似于在不查询数据库的情况下将 WHERE 子句应用于查询。

13-13.查询和存储大型对象

问题

您正在开发的应用需要存储可以包含无限数量字符的文本字符串。

解决办法

当需要存储的字符串非常大时,最好使用字符大对象(CLOB)数据类型来存储文本。RECIPE_TEXT 表的数据库图表如下:

RECIPE_TEXT (
id              int primary key,
recipe_id       int not null,
text            clob,
constraint recipe_text_fk
foreign key (recipe_id)
references recipes(id))

以下示例中的代码演示了如何将 CLOB 加载到数据库中以及如何查询它:

package org.java9recipes.chapter13.recipe13_13;

import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;

public class LobExamples {

    public static Connection conn = null;
    public static CreateConnection createConn;

    public static void main(String[] args) {
        boolean successFlag = false;
        try {
            createConn = new CreateConnection();
            conn = createConn.getConnection();
            loadClob();
            readClob();
        } catch (java.sql.SQLException ex) {
            System.out.println(ex);
        } finally {

            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }

        }
    }

    public static void loadClob() {
        Clob textClob = null;
        String sql = "INSERT INTO RECIPE_TEXT VALUES("
                    + "next value for recipe_text_seq, "
                    + "(select id from recipes where recipe_number = '13-1'), "
                    + "?)";
        try (PreparedStatement pstmt = conn.prepareStatement(sql);) {
            textClob = conn.createClob();
            textClob.setString(1, "This will be the recipe text in clob format");
            // obtain the sequence number in real world
            // set the clob value
            pstmt.setClob(1, textClob);
            pstmt.executeUpdate();

        } catch (SQLException ex) {
            ex.printStackTrace();
        }
    }

    public static void readClob() {
        String qry = "select text from recipe_text";
        Clob theClob = null;
        try(PreparedStatement pstmt = conn.prepareStatement(qry);
                ResultSet rs = pstmt.executeQuery();) {

            while (rs.next()) {
                theClob = rs.getClob(1);
                System.out.println("Clob length: " + theClob.length());
                System.out.println(theClob.toString());
            }
            System.out.println(theClob.toString());

        } catch (SQLException ex) {

            ex.printStackTrace();
        }
    }
}

它是如何工作的

如果您的应用需要存储字符串值,您需要知道这些字符串可能会有多大。对于 VARCHAR 字段的存储大小,大多数数据库都有一个上限。例如,Oracle 数据库的上限为 2,000 个字符,超过这个长度的部分将被删除。如果需要存储大量文本,请在数据库中使用 CLOB 字段。

在 Java 代码中,CLOB 的处理方式与字符串略有不同。事实上,在最初几次使用它时,实际上有点奇怪,因为您必须从一个连接创建 CLOB。

注意

实际上,CLOBs 和 BLOBs(二进制大型对象)并不存储在定义它们的 Oracle 表中。相反,大型对象(LOB)定位器存储在表列中。Oracle 可能会将 CLOB 放在数据库服务器上的一个单独文件中。当 Java 创建 Clob 对象时,它可以用来保存更新到数据库中特定 lob 位置的数据,或者从数据库中特定 LOB 位置检索数据。

让我们来看看这个菜谱的解决方案中包含的 loadClob()方法。如您所见,Clob 对象是使用 Connection createClob()方法创建的。一旦创建了 Clob,就可以使用 setString()方法设置它的内容,方法是传递指示放置字符串的位置和文本字符串本身:

textClob = conn.createClob();
textClob.setString(1, "This will be the recipe text in clob format");

一旦创建并填充了 Clob,只需使用 PreparedStatement setClob()方法将其传递给数据库。在本例中,PreparedStatement 像往常一样通过调用 executeUpdate()方法在 RECIPE_TEXT 表中执行数据库插入。

查询 Clob 也相当简单。正如您在这个配方的解决方案中包含的 readClob()方法中看到的,建立了一个 PreparedStatement 查询,并将结果检索到一个 ResultSet 中。使用 Clob 和字符串之间的唯一区别是,您必须将 Clob 加载到 Clob 类型中。

注意

调用 Clob getString()方法将传递一个看起来很有趣的文本字符串,它表示一个 Clob 对象。因此,调用 Clob 对象的 getAsciiStream()方法将返回存储在 Clob 中的实际数据。

虽然 Clobs 很容易使用,但是需要额外的几个步骤来准备。最好相应地规划您的应用,并尝试估计您使用的数据库字段是否由于大小限制而需要 CLOBs。适当的规划将防止您回头修改标准的基于字符串的代码来使用 Clobs。

13-14.调用存储过程

问题

应用所需的一些逻辑被编写为数据库存储过程。您需要能够从应用中调用存储过程。

解决办法

下面的代码块显示了创建 Java 将调用的存储过程所需的 PL/SQL。这个存储过程的功能非常小;它只是接受一个值,并将该值赋给一个 OUT 参数,以便程序可以显示它:

create or replace procedure dummy_proc (text IN VARCHAR2,
                                        msg OUT VARCHAR2) as
begin
    -- Do something, in this case the IN parameter value is assigned to the OUT parameter
    msg :=text;
end;

下面代码中的 CallableStatement 执行数据库中包含的这个存储过程,并传递必要的参数。然后,OUT 参数的结果会显示给用户。

try(CallableStatement cs = conn.prepareCall("{call DUMMY_PROC(?,?)}");) {       
    cs.setString(1, "This is a test");
    cs.registerOutParameter(2, Types.VARCHAR);
    cs.executeQuery();

    System.out.println(cs.getString(2));

} catch (SQLException ex){
    ex.printStackTrace();
}

运行这个菜谱的示例类将显示以下输出,它与输入相同。这是因为 DUMMY_PROC 过程只是将 IN 参数的内容分配给 OUT 参数。

Successfully connected
This is a test

它是如何工作的

对于应用来说,将数据库存储过程用于可以在数据库中直接执行的逻辑并不少见。为了从 Java 调用数据库存储过程,必须创建一个 CallableStatement 对象,而不是使用 PreparedStatement。在这个配方的解决方案中,一个 CallableStatement 调用一个名为 DUMMY_PROC 的存储过程。实例化 CallableStatement 的语法类似于使用 PreparedStatement 的语法。使用 Connection 对象的 prepareCall()方法,将调用传递给存储过程。存储过程调用必须用花括号{}括起来,否则应用将引发异常。

cs = conn.prepareCall("{call DUMMY_PROC(?,?)}");

一旦实例化了 CallableStatement,就可以像 PreparedStatement 一样使用它来设置参数值。但是,如果某个参数在数据库存储过程中注册为 OUT 参数,则必须调用一个特殊的方法 registerOutParameter(),传递要注册的 OUT 参数的参数位置和数据库类型。在这个配方的解决方案中,OUT 参数在第二个位置,它的类型是 VARCHAR。

cs.registerOutParameter(2, Types.VARCHAR);

若要执行存储过程,请对 CallableStatement 调用 executeQuery()方法。完成后,您可以通过调用对应于数据类型的 CallableStatement getXXX()方法来查看 OUT 参数的值:

    System.out.println(cs.getString(2));
关于存储函数的一个注记

调用存储数据库函数本质上与调用存储过程相同。但是,prepareCall()的语法略有修改。若要调用存储函数,请将花括号中的调用改为使用?性格。例如,假设一个名为 DUMMY_FUNC 的函数接受一个参数并返回一个值。下面的代码将用于进行调用并返回值:

cs = conn.prepareCall("{? = call DUMMY_FUNC(?)}");
cs.registerOutParameter(1, Types.VARCHAR);
cs.setString(2, "This is a test");
cs.execute();

对 cs.getString(1)的调用将检索返回值。

13-15.获取数据库使用的日期

问题

您希望正确转换 LocalDate,以便将其插入到数据库记录中。

解决办法

利用静态 java.sql.Date . value of(LocalDate)方法将 local date 对象转换为 Java . SQL . date 对象,JDBC 可以利用该对象插入或查询数据库。在下面的示例中,当前日期被插入到日期类型的数据库列中。

private static void insertRecord(
        String title,
        String publisher) {
    String sql = "INSERT INTO PUBLICATION VALUES("
            + "NEXT VALUE FOR PUBLICATION_SEQ, ?,?,?,?)";
    LocalDate pubDate = LocalDate.now();
    try (Connection conn = createConn.getConnection();
            PreparedStatement pstmt = conn.prepareStatement(sql);) {
        pstmt.setInt(1, 100);
        pstmt.setString(2, title);
        pstmt.setDate(3,  **java.sql.Date.valueOf(pubDate)**);
        pstmt.setString(4, publisher);
        pstmt.executeUpdate();
        System.out.println("Record successfully inserted.");
    } catch (SQLException ex) {
        ex.printStackTrace();
    }
}

它是如何工作的

在 Java 8 中,新的日期时间 API(第四章第四章)是处理日期和时间的首选 API。因此,当处理日期值和数据库时,JDBC API 必须在 SQL 日期和新的日期时间 LocalDate 对象之间进行转换。这个配方的解决方案演示了如何从 LocalDate 对象获取 java.sql.Date 的实例,只需调用静态 java.sql.Date.valueOf()方法,传递相关的 LocalDate 对象。

13-16.自动关闭资源

问题

与其在每次数据库调用时手动打开和关闭资源,不如让应用为您处理这些样板代码。

解决办法

使用 try-with-resources 语法自动关闭您打开的资源。下面的代码块使用这种策略,在使用完连接、语句和结果集资源后自动关闭它们:

String qry = "select recipe_number, recipename, description from recipes";

try (Connection conn = createConn.getConnection();
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(qry);) {

    while (rs.next()) {
        String recipe = rs.getString("RECIPE_NUMBER");
        String name = rs.getString("RECIPE_NAME");
        String desc = rs.getString("DESCRIPTION");

        System.out.println(recipe + "\t" + name + "\t" + desc);
    }
} catch (SQLException e) {
    e.printStackTrace();
}

运行此代码产生的输出应该类似于以下内容:

Successfully connected
13-1    Connecting to a Database        DriverManager and DataSource Implementations - More to Come
13-2    Querying a Database and Retrieving Results       Subject to Change
13-3    Handling SQL Exceptions Using SQLException

它是如何工作的

管理 JDBC 资源一直是件令人头疼的事情。当不再需要资源时,关闭资源需要大量的样板代码。自从 Java SE 7 发布以来,情况就不是这样了。Java 7 引入了使用 try-with-resources 的自动资源管理。通过使用这种技术,开发人员不再需要手动关闭每个资源,这种改变可以减少许多行代码。

为了使用这种技术,您必须实例化您希望在 try 子句后的一组括号中启用自动处理的所有资源。在这个配方的解决方案中,声明的资源是连接、语句和结果集。

try (Connection conn = createConn.getConnection();
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(qry);) {

一旦这些资源超出范围,它们就会自动关闭。这意味着不再需要编写 finally 块来确保关闭资源。自动资源处理不仅适用于数据库工作,还适用于任何符合新的 java.lang.Autocloseable API 的资源。文件 I/O 等其他操作也遵循新的 API。java.lang.Autoclosable 中有一个 close()方法管理资源的关闭。实现 java.io.Closeable 接口的类可以遵守 API。

摘要

在许多应用中,数据库对于存储重要信息已经变得必不可少。因此,很好地理解如何在应用中使用数据库是很重要的。本章从头开始,涵盖了数据库访问入门的诀窍。然后讨论了一些重要的主题,比如如何安全地访问和修改数据、事务管理以及在没有连接到网络时的数据访问。现在,您应该对 Java 解决方案中的一些数据处理技术有了很好的理解。请记住,有许多数据访问解决方案,本章中的方法只是解决信息管理这个大问题的一些方法。

十四、JavaFX 基础知识

JavaFX 8 API 是 Java 的富客户端 GUI 工具包,供开发人员构建跨平台应用。JavaFX 8 是对 JavaFX 2.2 的更新,它基于场景图范式(保留模式),而不是传统的即时模式风格渲染。JavaFX 的场景图是一种树状的数据结构,维护基于矢量的图形节点。JavaFX 的目标是跨多种类型的设备使用,如移动设备、智能手机、电视、平板电脑和桌面。在 JavaFX 的早期,小程序用于使 JavaFX 通过 web 可用,在移动设备上使用还不可能,但现在这些限制已经成为过去,JavaFX 在设备上有了更大的吸引力。

在 JavaFX 创建之前,富互联网应用的开发需要收集许多独立的库和 API 来实现功能强大的应用。这些独立的库包括媒体、UI 控件、Web、3D 和 2D API。因为集成这些 API 可能相当困难,Sun Microsystems(现在的 Oracle)的天才工程师创建了一组新的 JavaFX 库,将所有相同的功能组合在一个屋檐下。JavaFX 是 GUI 的瑞士军刀。JavaFX 8 是一个纯 Java(语言)API,允许开发人员利用现有的 Java 库和工具来开发可以在任何地方使用的应用。

根据你和谁交谈,你可能会遇到不同的“用户体验”的定义(或者在 UI 世界,UX)。但是有一个事实仍然存在:用户总是会要求 GUI 应用提供更好的内容和更高的可用性。鉴于这一事实,开发人员和设计人员经常一起工作来设计应用以满足这一需求。JavaFX 提供了一个工具包,使开发人员和设计人员(在许多情况下,他们是同一个人)能够创建功能强大但美观的应用。另一件需要承认的事情是,如果您正在开发一个游戏、媒体播放器或普通的企业应用,JavaFX 不仅会帮助开发更丰富的 ui,而且您还会发现这些 API 设计得非常好,可以极大地提高开发人员的工作效率。

有很多关于 JavaFX 的书籍,在短短几章中涵盖该工具包的所有功能是不可能的。希望本书中的食谱能够通过提供实际和真实世界的例子来引导你走向正确的方向。我鼓励您探索其他资源来进一步了解 JavaFX。我强烈推荐以下几本书:Pro JavaFX Platform (Apress,2009)、Pro JavaFX 2.0 Platform (Apress,2012)、Pro JavaFX 8 (Apress,2014)、和 JavaFX 8:举例介绍(Apress,2014)。这些书深入帮助你创建专业级应用。在本章中,您将学习 JavaFX 的基础知识,以便快速开发富互联网应用。它为您使用 JavaFX 提供了坚实的基础。

注意

对于 JavaFX 8 之前的 JavaFX 版本,SDK 是从标准 JDK 中单独下载的。也就是说,JavaFX 1.x 和 2.x SDKs 必须单独下载和安装。JavaFX 8 改变了这一要求,因为它是 JDK 8 的一部分。本书仅涵盖 JavaFX 8,尽管许多解决方案可能在 JavaFX 2.x 上正常运行。如果您需要安装 JavaFX 2.x,请参考在线文档(docs.oracle.com/javafx/)或涵盖 JavaFX 2.x 的书籍,例如由 Carl Dea 编写并由 Apress 出版的 Java FX 2.0:Introduction by Example。要查看 JavaFX 8 在线文档,请访问docs . Oracle . com/javase/8/javase-client technologies . htm

14-1.创建简单的用户界面

问题

您希望创建、编码、编译和运行一个简单的 JavaFX Hello World 应用。

解决方案 1

使用 NetBeans IDE 中的 JavaFX 项目创建向导开发 JavaFX Hello World 应用。

在 NetBeans 中创建 JavaFX Hello World 应用

要使用 NetBeans IDE 快速开始创建、编码、编译和运行简单的 JavaFX Hello World 应用,请按照下列步骤操作:

  1. 启动 NetBeans IDE。

  2. 从“文件”菜单中,选择“新建项目”。

  3. 在“选择项目和类别”下,选择 JavaFX 文件夹。

  4. 在“项目”下,选择 JavaFX 应用,然后单击“下一步”。

    注意如果这是您在 NetBeans 中的第一个 JavaFX 项目,JavaFX 模块可能会在此时自动激活。

  5. 指定 HelloWorldMain 作为您的项目名称。

  6. 更改或接受“项目位置”和“项目文件夹”字段的默认值。

  7. 确保 JavaFX 平台设置为 JDK 1.9。保持选中创建自定义预加载程序框,因为它会自动生成加载和运行应用所需的代码。

  8. 确保选择了“创建应用类”选项。单击完成。

  9. 在 NetBeans IDE 的“项目”选项卡上,选择新创建的项目。打开“项目属性”对话框,验证源/二进制格式设置是否为 JDK 9。单击类别下的来源。

  10. 关闭“Java 平台管理器”窗口后,单击“确定”关闭“项目属性”窗口。

要运行和测试 JavaFX Hello World 应用,请访问 run 菜单并选择 Run Main Project。您也可以右键单击项目目录,然后从上下文菜单中选择“运行”。

图 14-1 显示了一个从 NetBeans IDE 启动的简单 JavaFX Hello World 应用。

A323910_3_En_14_Fig1_HTML.jpg

图 14-1。从 NetBeans IDE 启动的 JavaFX Hello World

解决方案 2

使用您最喜欢的编辑器编写 JavaFX Hello World 应用。一旦创建了 Java 文件,您将使用命令行提示符来编译和运行 JavaFX 应用。下面是创建 JavaFX Hello World 应用的步骤,该应用将在命令行提示符下编译和运行。

在文本编辑器中创建 JavaFX Hello World 应用

要快速开始:

  1. 将下面的代码复制粘贴到你最喜欢的编辑器中,并将文件另存为 HelloWorldMain.java。

    以下是 JavaFX Hello World 应用的源代码:

    package org.java9recipes.chapter14.recipe14_01;
    
    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.stage.Stage;
    import javafx.scene.Group;
    public class HelloWorldMain extends Application {
    
        final Group root = new Group();
        /**
         * @param args the command line arguments
         */
        public static void main(String[] args) {
            Application.launch(args);
        }
    
        @Override
        public void start(Stage primaryStage) {
            primaryStage.setTitle("Hello World");
            Scene scene = new Scene(root, 300, 250);
            Button btn = new Button();
            btn.setLayoutX(100);
            btn.setLayoutY(80);
            btn.setText("Hello World");
            btn.setOnAction((event) -> {
                    System.out.println("Hello World");
            });
            root.getChildren().add(btn);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    }
    
  2. 保存名为 HelloWorldMain.java 的文件后,使用命令行提示符导航到该文件。

  3. 使用 Java 编译器 javac 编译源代码文件 HelloWorldMain.java:

    javac -d . HelloWorldMain.java
    
  4. 运行并测试您的 JavaFX Hello World 应用。假设您与 HelloWorldMain.java 文件位于同一个目录中,请在命令行提示符下键入以下命令来运行 JavaFX Hello World 应用:

    java org.java9recipes.chapter14.recipe14_01.HelloWorldMain
    
注意

该类也可以在现有的 JDK 8 或 JDK 9 应用中创建。例如,包含本书源代码的项目包含 org . Java 9 recipes . chapter14 源代码包中的所有 JavaFX 食谱。这是可能的,因为 JavaFX 不再需要额外的配置;它已经是任何 JDK 8 或 JDK 9 项目的一部分。

它是如何工作的

以下是对这两种解决方案的描述。这两种解决方案都需要 JavaFX 8(或者 JavaFX 2.x 和 JDK 7)。解决方案 1 演示了如何使用 NetBeans IDE 构建 JavaFX 应用。解决方案 2 包括通过您喜欢的文本编辑器开发一个简单的 JavaFX 应用,并使用命令行或终端来编译和执行 JavaFX 程序。

NetBeans IDE 使得通过 JavaFX 项目开发 JavaFX 应用变得非常容易。事实上,在遵循 JavaFX 项目创建向导之后,NetBeans 提供了一个模板 Hello World 应用。对于开始任何 JavaFX 应用来说,这都是一个很好的解决方案,因为它为构建更复杂的解决方案提供了一个很好的起点。

要使用您最喜欢的文本编辑器创建一个简单的 JavaFX Hello World 应用,请遵循解决方案 2 的步骤 1 和 2。要在命令行上编译和运行 Hello World 程序,请遵循解决方案 2 的步骤 3 和 4。将源代码输入到您喜欢的编辑器中并保存源文件后,编译并运行 JavaFX 程序。打开命令行或终端窗口,导航到名为 HelloWorldMain.java 的 Java 文件的目录位置。

接下来,我们回顾一种使用 javac-d . HelloWorldMain.java 命令编译文件的方法。您会注意到文件名前面的-d。这让 Java 编译器根据包名知道将类文件放在哪里。在这个场景中,HelloWorldMain 包语句是 helloworldmain,它将在当前目录下创建一个子目录。以下命令将编译并运行 JavaFX Hello World 应用:

cd \<path to project>\org\java9recipes\chapter14\recipe14_01

javac –d . HelloWorldMain.java

java helloworldmain.HelloWorldMain
注意

有许多方法可以打包和部署 JavaFX 应用。要了解更多信息,请参阅“了解如何部署和打包 JavaFX 应用”,网址为。有关 JavaFX 部署策略的详细信息,请参见位于的 Oracle“部署指南”http://docs . Oracle . com/javase/9/docs/technotes/guides/deploy/

在这两个解决方案中,您会在源代码中注意到 JavaFX 应用扩展了 javafx.application. Application 类。Application 类提供应用生命周期功能,例如在运行时启动和停止。这也为 Java 应用提供了一种以线程安全的方式启动 JavaFX GUI 组件的机制。请记住,与 Java Swing 的事件调度线程(EDT)同义,JavaFX 有自己的 JavaFX 应用线程。JavaFX 8 中的新功能是,EDT 和 JavaFX 应用线程可以合并(见配方 14-18)。

看一下代码,在 main()方法的入口点,只需将命令行参数传递给 Application.launch()方法,就可以启动 JavaFX 应用。一旦应用处于就绪状态,框架内部将调用 start()方法开始。当调用 start()方法时,JavaFX javafx.stage.Stage 对象可供开发人员使用和操作。

你会注意到一些对象的名字很奇怪,比如舞台和场景。API 的设计者模拟了类似于剧院或戏剧的东西,演员在观众面前表演。用同样的类比,为了表现一部剧,演员表演的场景基本上都是一对多的。当然,所有的场景都是在舞台上表演的。在 JavaFX 中,Stage 相当于一个类似于 Java Swing API JFrame 或 JDialog 的应用窗口。您可能会将场景对象视为能够容纳零到多个节点对象的内容窗格。节点是所有要渲染的场景图节点的基本基类。场景图是一种树形数据结构,它维护着作为应用一部分的所有节点或图形对象的内部模型。常用的节点是 UI 控件和 Shape 对象。类似于树数据结构,场景图将通过使用容器类组来包含子节点。稍后在查看 ObservableList 时,您将了解到更多关于 Group 类的信息,但是现在,请将它们视为能够保存节点的 Java 列表或集合。

添加子节点后,设置 primaryStage 的(Stage)场景并调用 Stage 对象上的 show()方法来显示 JavaFX 窗口。

最后一件事:在这一章中,大多数示例应用的结构与这个示例相同,其中配方代码解决方案将驻留在 start()方法中。这一章中的大多数食谱遵循同样的模式。为了简洁起见,许多样板代码没有显示。要查看所有食谱的完整源代码列表,请从该书的网站下载源代码。

14-2.绘图文本

问题

您希望在 JavaFX 应用中绘制自定义文本。

解决办法

利用 javafx.scene.text.Text 类创建要放置在 JavaFX 场景图上的文本节点。由于文本节点将被放置在场景图形上,您决定要创建随机定位的文本节点,这些文本节点围绕其分散在场景区域的(x,y)位置旋转。

以下代码实现了一个 JavaFX 应用,该应用以随机位置和颜色显示散布在场景图形中的文本节点:

primaryStage.setTitle("Chapter 14-2 Drawing Text");
Group root = new Group();
Scene scene = new Scene(root, 300, 250, Color.WHITE);
Random rand = new Random(System.currentTimeMillis());
for (int i = 0; i < 100; i++) {
    int x = rand.nextInt((int) scene.getWidth());
    int y = rand.nextInt((int) scene.getHeight());
    int red = rand.nextInt(255);
    int green = rand.nextInt(255);
    int blue = rand.nextInt(255);

    Text text = new Text(x, y, "Java 9 Recipes");

    int rot = rand.nextInt(360);
    text.setFill(Color.rgb(red, green, blue, .99));
    text.setRotate(rot);
    root.getChildren().add(text);
}

primaryStage.setScene(scene);
primaryStage.show();

图 14-2 显示了散布在 JavaFX 场景图中的随机文本节点。

A323910_3_En_14_Fig2_HTML.jpg

图 14-2。在任意位置绘制文本

它是如何工作的

要在 JavaFX 中绘制文本,您需要创建一个 javafx.scene.text.Text 节点来放置在场景图(javafx.scene.Scene)上。在这个例子中,你会注意到散布在场景区域的随机颜色和位置的文本对象。

首先,创建一个循环来生成随机(x,y)坐标以定位文本节点。第二,创建(0–255 RGB)之间的随机颜色分量,应用于文本节点。第三,旋转角度(以度为单位)是一个在(0-360 度)之间随机生成的值,以使文本倾斜。以下代码创建随机值,这些值将被分配给文本节点的位置、颜色和旋转:

int x = rand.nextInt((int) scene.getWidth());
int y = rand.nextInt((int) scene.getHeight());
int red = rand.nextInt(255);
int green = rand.nextInt(255);
int blue = rand.nextInt(255);
int rot = rand.nextInt(360);

一旦生成了随机值,它们将被应用到文本节点,这些节点将被绘制到场景图上。以下代码片段将位置(x,y)、颜色(RGB)和旋转(角度以度为单位)应用于文本节点:

Text text = new Text(x, y, "Java 9 Recipes");
text.setFill(Color.rgb(red, green, blue, .99));
text.setRotate(rot);

root.getChildren().add(text);

你将会通过它的易用性开始看到场景图形 API 的强大。文本节点可以像形状一样轻松操作。实际上它们是形状。在继承层次结构中定义的文本节点从 javafx.scene.shape.Shape 类扩展而来,因此能够做一些有趣的事情,比如用颜色填充或旋转一个角度。虽然文本是彩色的,但这仍然有点乏味。然而,在下一个食谱中,你将学习如何改变文本的字体。

14-3.更改文本字体

问题

您想要更改文本字体并为文本节点添加特殊效果。

解决方案 1

创建一个 JavaFX 应用,该应用使用以下类来设置文本字体并将嵌入效果应用于文本节点:

javafx.scene.text.Font 
javafx.scene.effect.DropShadow 
javafx.scene.effect.Reflection 

下面的代码设置字体并将效果应用于文本节点。它使用 Serif、SanSerif、Dialog 和等宽字体以及投影和反射效果:

primaryStage.setTitle("Chapter 14-3 Changing Text Fonts");
Group root = new Group();
Scene scene = new Scene(root, 330, 250, Color.WHITE);

// Serif with drop shadow
Text java9recipes2 = new Text(50, 50, "Java 9 Recipes");
Font serif = Font.font("Serif", 30);
java9recipes2.setFont(serif);
java9recipes2.setFill(Color.RED);
DropShadow dropShadow = new DropShadow();
dropShadow.setOffsetX(2.0f);
dropShadow.setOffsetY(2.0f);
dropShadow.setColor(Color.rgb(50, 50, 50, .588));
java9recipes2.setEffect(dropShadow);
root.getChildren().add(java9recipes2);

// SanSerif
Text java9recipes3 = new Text(50, 100, "Java 8 Recipes");
Font sanSerif = Font.font("SanSerif", 30);
java9recipes3.setFont(sanSerif);
java9recipes3.setFill(Color.BLUE);
root.getChildren().add(java9recipes3);

// Dialog
Text java9recipes4 = new Text(50, 150, "Java 8 Recipes");
Font dialogFont = Font.font("Dialog", 30);
java9recipes4.setFont(dialogFont);
java9recipes4.setFill(Color.rgb(0, 255, 0));
root.getChildren().add(java9recipes4);

// Monospaced
Text java9recipes5 = new Text(50, 200, "Java 8 Recipes");
Font monoFont = Font.font("Monospaced", 30);
java9recipes5.setFont(monoFont);
java9recipes5.setFill(Color.BLACK);
root.getChildren().add(java9recipes5);

Reflection refl = new Reflection();
refl.setFraction(0.8f);
java9recipes5.setEffect(refl);

primaryStage.setScene(scene);
primaryStage.show();

图 14-3 显示了应用于文本节点的各种字体样式和效果(阴影和倒影)的 JavaFX 应用。

A323910_3_En_14_Fig3_HTML.jpg

图 14-3。更改文本字体

解决方案 2

利用新的 TextFlow 节点来帮助将富文本串在一起。使用 FXML 文件构建对象图,然后将级联样式表(CSS)样式应用于 FXML 中的图节点。对于那些更喜欢使用标记语言而不是 Java 代码的人来说,这个解决方案提供了一个更好的途径。它还演示了如何使用样式表来声明应用的样式。

首先,让我们看看用于构造布局的 FXML。以下几行标记构建了一个场景图,其中包含一个包含 TextFlow 的窗格。TextFlow 包含一系列文本节点,每个节点都应用了不同的样式。以下清单包含 textfonts.fxml 的源代码。

<?xml version="1.0" encoding="UTF-8"?>

<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>

<Scene width="200" height="75" fill="white" xmlns:fx="http://javafx.com/fxml">
    <stylesheets>
        <URL value="@textfonts.css"/>
    </stylesheets>
    <Pane fx:id="pane">

         <TextFlow styleClass="mainmessage">
            <Text styleClass="span1">Hello </Text>
            <Text text=" "/>
            <Text styleClass="span2, large">Java</Text>
            <Text styleClass="span3, slant">FX</Text>
            <Text text=" "/>
            <Text styleClass="cool">8</Text>

        </TextFlow>
    </Pane>
</Scene>

在 FXML 中,导入名为 textfonts.css 的 CSS。下面的列表包含位于 textfonts.css 中的样式。

.mainmessage {
    -fx-font-family: "Helvetica";
    -fx-font-size: 30px;
}

.span1 {
    -fx-color: "red";
}

.span2 {
    -fx-font-family: "Serif";
    -fx-font-size: 30px;
    -fx-color: "red";
}

.span3 {
    -fx-font-family: "Serif";
    -fx-font-size: 30px;
    -fx-fill: "orange";
    -fx-font-style: italic;
}

.cool {
    -fx-effect: dropshadow(gaussian, gray, 8, 0.5, 8, 8);
}

最后,使用一个标准的 JavaFX 应用类来实例化该示例。以下资料来自 ChangingTextFontsSolution2.java,它们演示了如何加载 FXML 和构建 stage。

@Override
public void start(Stage stage) throws Exception {
    stage.setTitle("Chapter 14-3 Changing Text Fonts Using TextFlow and FXML");
    stage.setScene((Scene) FXMLLoader.load(getClass().getResource("textfonts.fxml")));
    stage.show();
}

生成的应用将渲染一个类似于图 14-4 所示结果的场景。

A323910_3_En_14_Fig4_HTML.jpg

图 14-4。TextFlow 和 FXML

它是如何工作的

解决方案 1 演示了如何使用标准 Java 代码将字体应用于文本。基于矢量的图形允许您缩放形状和应用效果,而没有像素化(锯齿)的问题。JavaFX 节点使用基于矢量的图形。在每个文本节点中,您可以创建和设置要渲染到场景图形上的字体。下面是在文本节点上创建和设置字体的代码:

Text java9recipes2 = new Text(50, 50, "Java 9 Recipes");
Font serif = Font.font("Serif", 30);
Java9recipes2.setFont(serif);

在解决方案 1 中,投影是一个真实效果(drop shadow)对象,实际上应用于单个文本节点实例。DropShadow 对象被设置为基于相对于文本节点的 x 和 y 偏移进行定位。你也可以设置阴影的颜色;这里我们设置它为灰色,不透明度为 0.588。以下是使用投影效果(drop shadow)设置文本节点的 effect 属性的示例:

DropShadow dropShadow = new DropShadow();
dropShadow.setOffsetX(2.0f);
dropShadow.setOffsetY(2.0f);
dropShadow.setColor(Color.rgb(50, 50, 50, .588));
java9recipes2.setEffect(dropShadow);

虽然这个菜谱是关于设置文本字体的,但是它也将效果应用到了文本节点。又增加了一个效果(只是提升了一个档次)。当使用等宽字体创建最后一个文本节点时,应用了流行的反射效果。代码后的代码被设置为显示 0.8 或 80%的反射。反射值的范围从零(0%)到一(100%)。以下代码片段实现了浮点值为 0.8f 的 80%反射:

Reflection refl = new Reflection();
refl.setFraction(0.8f);
java9recipes5.setEffect(refl);

解决方案 2 演示了如何使用 FXML、CSS 和 Java 构建用户界面。虽然这份食谱侧重于文本和字体,但重要的是要注意 FXML 解决方案显然遵循模型-视图-控制器标准,将 UI 代码与业务逻辑分离开来。同样需要注意的是,如果本例中的 UI 包含按钮或其他包含动作的节点,那么也需要创建一个控制器类来实现动作逻辑。

在第二个示例中,FXML 文件包含用户界面的结构化布局,该布局由场景、窗格、TextFlow 和一系列文本节点组成。该场景包含一个元素,用于指定将哪些样式表应用于 XML 中的元素。Pane 节点用作布局的基础,它包含 UI 中的每个其他节点。TextFlow 节点是在 JavaFX 8 中引入的,它是一种特殊的布局,用于布局富文本。TextFlow 可以将许多不同的文本节点放入单个流中。

正如您在 FXML 中看到的,TextFlow 中的每个文本节点都有不同的关联样式,这是基于附加样式表中定义的样式。JavaFX 样式表中样式的属性以–FX-开头,属性名和值由冒号分隔,以分号(;).在很大程度上,JavaFX 样式属性与标准 CSS 属性非常一致。有关完整的摘要,请参考位于docs . Oracle . com/Java FX/2/CSS _ tutorial/jfxpub-CSS _ tutorial . htm的文档。

TextFlow 使用嵌入其中的每个节点的文本和字体,以及它自己的宽度和文本对齐方式来确定文本的位置。文本之外的节点也可以嵌入到 TextFlow 中。向 TextFlow 添加文本节点时,可以通过 setMaxWidth()方法指定 TextFlow 的最大宽度来设置自动换行。也可以在文本节点中的任何字符串末尾包含一个\n 来开始换行。以下代码执行与 1 相同的解决方案,但使用 TextFlow 来布局文本节点,而不是分别将每个节点添加到场景图中。

primaryStage.setTitle("Chapter 14-3 Changing Text Fonts");
Group root = new Group();
Scene scene = new Scene(root, 330, 250, Color.WHITE);

// Serif with drop shadow
Text java9recipes2 = new Text(50, 50, "Java 9 Recipes");
Font serif = Font.font("Serif", 30);
java9recipes2.setFont(serif);
java9recipes2.setFill(Color.RED);
DropShadow dropShadow = new DropShadow();
dropShadow.setOffsetX(2.0f);
dropShadow.setOffsetY(2.0f);
dropShadow.setColor(Color.rgb(50, 50, 50, .588));
java9recipes2.setEffect(dropShadow);

// SanSerif
Text java9recipes3 = new Text(50, 100, "Java 8 Recipes\n");
Font sanSerif = Font.font("SanSerif", 30);
java9recipes3.setFont(sanSerif);
java9recipes3.setFill(Color.BLUE);

// Dialog
Text java9recipes4 = new Text(50, 150, "Java 8 Recipes\n");
Font dialogFont = Font.font("Dialog", 30);
java9recipes4.setFont(dialogFont);
java9recipes4.setFill(Color.rgb(0, 255, 0));

// Monospaced
Text java9recipes5 = new Text(50, 200, "Java 8 Recipes");
Font monoFont = Font.font("Monospaced", 30);
java9recipes5.setFont(monoFont);
java9recipes5.setFill(Color.BLACK);

Reflection refl = new Reflection();
refl.setFraction(0.8f);
java9recipes5.setEffect(refl);
TextFlow flow = new TextFlow(java9recipes2, java9recipes3, java9recipes4, java9recipes5);

root.getChildren().add(flow);

这个食谱中引入了很多概念。您将在后面的菜谱中了解更多关于 FXML 的内容,或者更多信息,您可以在 http://docs . Oracle . com/Java FX/2/get _ started/FXML:tutorial . htm 上查看在线文档。你可以在docs . Oracle . com/javase/8/Java FX/API/Java FX/scene/text/TextFlow . html阅读文档了解更多关于 TextFlow 布局的信息。

14-4.创建形状

问题

您希望创建放置在场景图上的形状。

解决办法

在 javafx.scene.shape.*包中使用 JavaFX 的 Arc、Circle、CubicCurve、Ellipse、Line、Path、Polygon、Polyline、QuadCurve、Rectangle、SVGPath 和 Text 类。下面的代码绘制了各种复杂的形状。第一个复杂的形状包括一条以正弦波形状绘制的三次曲线。下一个形状称为冰淇淋甜筒,它使用包含路径元素的 path 类(javafx.scene.shape.PathElement)。第三个形状是二次贝塞尔曲线(QuadCurve ),它形成一个微笑。最终的形状是一个美味的甜甜圈。您可以通过减去两个椭圆(一个较小,一个较大)来创建这个圆环形状:

@Override
public void start(Stage primaryStage) {
    primaryStage.setTitle("Chapter 14-4 Creating Shapes");
    Group root = new Group();
    Scene scene = new Scene(root, 306, 550, Color.WHITE);

    // CubicCurve
    CubicCurve cubicCurve = new CubicCurve();
    cubicCurve.setStartX(50);
    cubicCurve.setStartY(75); // start pt (x1,y1)
    cubicCurve.setControlX1(80);
    cubicCurve.setControlY1(-25);// control pt1
    cubicCurve.setControlX2(110);
    cubicCurve.setControlY2(175);  // control pt2
    cubicCurve.setEndX(140);
    cubicCurve.setEndY(75);
    cubicCurve.setStrokeType(StrokeType.CENTERED);
    cubicCurve.setStrokeWidth(1);
    cubicCurve.setStroke(Color.BLACK);
    cubicCurve.setStrokeWidth(3);
    cubicCurve.setFill(Color.WHITE);

    root.getChildren().add(cubicCurve);

    // Ice cream
    Path path = new Path();

    MoveTo moveTo = new MoveTo();
    moveTo.setX(50);
    moveTo.setY(150);

    QuadCurveTo quadCurveTo = new QuadCurveTo();
    quadCurveTo.setX(150);
    quadCurveTo.setY(150);
    quadCurveTo.setControlX(100);
    quadCurveTo.setControlY(50);

    LineTo lineTo1 = new LineTo();
    lineTo1.setX(50);
    lineTo1.setY(150);

    LineTo lineTo2 = new LineTo();
    lineTo2.setX(100);
    lineTo2.setY(275);

    LineTo lineTo3 = new LineTo();
    lineTo3.setX(150);
    lineTo3.setY(150);
    path.getElements().add(moveTo);
    path.getElements().add(quadCurveTo);
    path.getElements().add(lineTo1);
    path.getElements().add(lineTo2);
    path.getElements().add(lineTo3);
    path.setTranslateY(30);
    path.setStrokeWidth(3);
    path.setStroke(Color.BLACK);

    root.getChildren().add(path);

    // QuadCurve create a smile
    QuadCurve quad = new QuadCurve();
    quad.setStartX(50);
    quad.setStartY(50);
    quad.setEndX(150);
    quad.setEndY(50);
    quad.setControlX(125);
    quad.setControlY(150);
    quad.setTranslateY(path.getBoundsInParent().getMaxY());
    quad.setStrokeWidth(3);
    quad.setStroke(Color.BLACK);
    quad.setFill(Color.WHITE);

    root.getChildren().add(quad);

    // outer donut
    Ellipse bigCircle = new Ellipse(100, 100, 50, 75/2);
    //bigCircle.setTranslateY(quad.getBoundsInParent().getMaxY());
    bigCircle.setStrokeWidth(3);
    bigCircle.setStroke(Color.BLACK);
    bigCircle.setFill(Color.WHITE);

    // donut hole
    Ellipse smallCircle = new Ellipse(100, 100, 35/2, 25/2);

    // make a donut
    Shape donut = Path.subtract(bigCircle, smallCircle);
    donut.setStrokeWidth(1);
    donut.setStroke(Color.BLACK);
    // orange glaze
    donut.setFill(Color.rgb(255, 200, 0));

    // add drop shadow
    DropShadow dropShadow = new DropShadow();
    dropShadow.setOffsetX(2.0f);
    dropShadow.setOffsetY(2.0f);
    dropShadow.setColor(Color.rgb(50, 50, 50, .588));

    donut.setEffect(dropShadow);

    // move slightly down for spacing
    donut.setTranslateY(quad.getBoundsInParent().getMinY() + 10);

    root.getChildren().add(donut);

    primaryStage.setScene(scene);
    primaryStage.show();
}

图 14-5 显示了使用 JavaFX 创建的正弦波、冰淇淋甜筒、微笑和甜甜圈形状。

A323910_3_En_14_Fig5_HTML.jpg

图 14-5。创建形状

它是如何工作的

在这个解决方案中,您生成了一些基本的 2D 形状。第一个形状是 javafx.scene.shape.CubicCurve 类,它允许您构造一个三次曲线(“曲线”)效果。要创建三次曲线,只需查找要实例化的适当构造函数。以下代码片段用于创建 javafx.scene.shape.CubicCurve 实例:

CubicCurve cubicCurve = new CubicCurve();
cubicCurve.setStartX(50);
cubicCurve.setStartY(75); // start pt (x1,y1)
cubicCurve.setControlX1(80);
cubicCurve.setControlY1(-25);// control pt1
cubicCurve.setControlX2(110);
cubicCurve.setControlY2(175);  // control pt2
cubicCurve.setEndX(140);
cubicCurve.setEndY(75);
cubicCurve.setStrokeType(StrokeType.CENTERED);
cubicCurve.setStrokeWidth(1);
cubicCurve.setStroke(Color.BLACK);
cubicCurve.setStrokeWidth(3);
cubicCurve.setFill(Color.WHITE);

首先实例化一个 CubicCurve()实例。接下来,通过利用对象的 setter 方法并向每个方法传递一个值,以任意顺序指定曲线的属性。在 CubicCurve()对象上指定值后,可以使用以下符号将其添加到场景图中:

root.getChildren().add(cubicCurve);

冰淇淋甜筒形状是使用 javafx.scene.shape.Path 类创建的。当每个 path 元素被创建并添加到 Path 对象中时,每个元素都不被视为图形节点(javafx.scene.Node)。这意味着它们不是从 javafx.scene.shape.Shape 类扩展的,并且不能是要显示的场景图中的子节点。当查看 Javadoc 时(请参见docs . Oracle . com/javase/8/Java FX/API/Java FX/scene/shape/Path . html),您会注意到 Path 类是从 Shape 类扩展而来的,Shape 类又是从(javafx.scene.Node)类扩展而来的,因此 Path 是一个图形节点,但是 Path 元素不是从 Shape 类扩展而来的。Path 元素实际上是从 javafx.scene.shape.PathElement 类扩展而来的,该类仅用于 Path 对象的上下文中。因此,您将无法实例化一个 LineTo 类来放入场景图中。只要记住以 To 作为后缀的类是路径元素,而不是真正的形状节点。例如,MoveTo 和 LineTo 对象实例是添加到 Path 对象的路径元素,而不是可以添加到场景中的形状。以下是添加到 Path 对象中用于绘制冰淇淋蛋卷的路径元素:

// Ice cream
Path path = new Path();

MoveTo moveTo = new MoveTo();
moveTo.setX(50);
moveTo.setY(150);

...// Additional Path Elements created.
LineTo lineTo1 = new LineTo();
lineTo1.setX(50);
lineTo1.setY(150);

...// Additional Path Elements created.

path.getElements().add(moveTo);
path.getElements().add(quadCurveTo);
path.getElements().add(lineTo1);

渲染 QuadCurve (smile)对象时,实例化一个新的 QuadCurve 对象,并相应地设置每个属性。同样,每个属性都接受一个值。

最后是有投影效果的美味甜甜圈形状,它实际上是由两个圆形椭圆创建的。通过从较大的椭圆区域中减去较小的椭圆(圆环孔),使用 Path.subtract()方法创建并返回一个新的派生形状。以下是使用 Path.subtract()方法创建圆环形状的代码片段:

// outer donut
Ellipse bigCircle = ...//Outer shape area

// donut hole
Ellipse smallCircle = ...// Inner shape area

// make a donut
Shape donut = Path.subtract(bigCircle, smallCircle);

接下来,投影效果被添加到甜甜圈。这一次,与前一个配方类似,不再绘制两次形状,而是绘制一次,并使用 setEffect()方法将 DropShadow 对象实例应用于圆环形状对象。与前面的技术类似,通过调用 setOffsetX()和 setOffsetY()来设置阴影的偏移。

注意

在以前的版本中,可以使用构建器对象更容易地创建形状。然而,由于性能和膨胀问题,构建器类被从 JavaFX 8+中移除。如果您正在维护利用构建器类的代码,建议您远离它们,使用标准对象,如本食谱所示。

14-5.给对象分配颜色

问题

您希望用简单的颜色和渐变颜色填充形状。

解决办法

在 JavaFX 中,所有形状都可以用简单颜色和渐变颜色填充。以下是用于填充形状节点的主要类:

javafx.scene.paint.Color
javafx.scene.paint.LinearGradient
javafx.scene.paint.Stop
javafx.scene.paint.RadialGradient

下面的代码使用前面的类向形状添加径向和线性渐变颜色以及透明(alpha 通道级别)颜色。这个食谱使用了一个椭圆、一个矩形和一个圆角矩形。一条黑色实线(如图 14-5 所示)也出现在配方中,以展示形状颜色的透明度。

public void start(Stage primaryStage) {
    primaryStage.setTitle("Chapter 14-5 Assigning Colors To Objects");
    Group root = new Group();
    Scene scene = new Scene(root, 350, 300, Color.WHITE);

    Ellipse ellipse = new Ellipse(100, 50 + 70/2, 50, 70/2);
    RadialGradient gradient1 = new RadialGradient(0,
                                                  .1,    // focus angle
                                                  80,    // focus distance
                                                  45,    // centerX
                                                  120,   // centerY
                                                  false, // proportional
                                                  CycleMethod.NO_CYCLE,
                                                  new Stop(0, Color.RED), new Stop(1, Color.BLACK));

    ellipse.setFill(gradient1);
    root.getChildren().add(ellipse);

    // Create line
    Line blackLine = new Line();
    blackLine.setStartX(170);
    blackLine.setStartY(30);
    blackLine.setEndX(20);
    blackLine.setEndY(140);
    blackLine.setFill(Color.BLACK);
    blackLine.setStrokeWidth(10.0f);
    blackLine.setTranslateY(ellipse.prefHeight(-1) + ellipse.getLayoutY() + 10);

    root.getChildren().add(blackLine);

    // Create rectangle
    Rectangle rectangle = new Rectangle();
    rectangle.setX(50);
    rectangle.setY(50);
    rectangle.setWidth(100);
    rectangle.setHeight(70);
    rectangle.setTranslateY(ellipse.prefHeight(-1) + ellipse.getLayoutY() + 10);

    // Create linear gradient
    LinearGradient linearGrad = new LinearGradient(
          50,     //startX
          50,     //startY
          50,     //endX
          50 + rectangle.prefHeight(-1) + 25,     //endY
          false,  //proportional
          CycleMethod.NO_CYCLE,
          new Stop(0.1f, Color.rgb(255, 200, 0, .784)),
          new Stop(1.0f, Color.rgb(0, 0, 0, .784)));

    rectangle.setFill(linearGrad);
    root.getChildren().add(rectangle);

    // Create rectangle with rounded corners
    Rectangle roundRect = new Rectangle();
    roundRect.setX(50);
    roundRect.setY(50);
    roundRect.setWidth(100);
    roundRect.setHeight(70);
    roundRect.setArcWidth(20);
    roundRect.setArcHeight(20);
    roundRect.setTranslateY(ellipse.prefHeight(-1) +
                        ellipse.getLayoutY() +
                        10 +
                        roundRect.prefHeight(-1) +
                        roundRect.getLayoutY() + 10);

    LinearGradient cycleGrad = new LinearGradient(50,
                                                  50,
                                                  70,
                                                  70,
                                                  false,
                                                  CycleMethod.REFLECT,
                                                  new Stop(0f, Color.rgb(0, 255, 0, .784)),
                                                  new Stop(1.0f, Color.rgb(0, 0, 0, .784)));

    roundRect.setFill(cycleGrad);
    root.getChildren().add(roundRect);

    primaryStage.setScene(scene);
    primaryStage.show();
}

图 14-6 显示了可应用于形状的各种彩色填充。

A323910_3_En_14_Fig6_HTML.jpg

图 14-6。颜色形状

它是如何工作的

图 14-5 显示了从上到下显示的形状,从椭圆形、矩形和带有彩色渐变填充的圆角矩形开始。当绘制 eclipse 形状时,您将使用径向渐变,看起来好像是一个 3D 球形对象。接下来,创建一个用黄色半透明线性渐变填充的矩形。在黄色矩形后面绘制了一条粗黑线,以展示矩形的半透明颜色。最后,生成一个圆角矩形,该矩形在对角线方向上填充有类似 3D 管的绿黑反射线性渐变。

带有渐变色彩的神奇之处在于,它们常常能让形状看起来很立体。渐变绘画允许您在两种或更多种颜色之间进行插值,从而赋予形状深度。JavaFX 提供了两种类型的渐变:径向渐变(RadialGradient)和线性渐变(LinearGradient)。在示例中,径向渐变(radial gradient)应用于椭圆形状。

表 14-1 取自为 RadialGradient 类找到的 JavaFX 8 Javadoc 定义(docs . Oracle . com/javase/8/Java FX/API/Java FX/scene/paint/radial gradient . html)。

表 14-1。辐射梯度属性
|

财产

|

数据类型

|

描述

|
| --- | --- | --- |
| 聚焦角度 | 两倍 | 从渐变中心到第一种颜色映射到的焦点的角度,以度为单位 |
| 焦距 | 两倍 | 从渐变中心到第一种颜色映射到的焦点的距离 |
| 中心(c) | 两倍 | 渐变圆中心点的 x 坐标 |
| 百年 | 两倍 | 渐变圆中心点的 y 坐标 |
| 半径 | 两倍 | 定义颜色渐变范围的圆的半径 |
| 相称的 | 布尔 | 坐标和大小与渐变填充的形状成比例 |
| 循环法 | 循环法 | 应用于渐变的循环方法 |
| 不透明光圈 | 布尔列表 | 油漆是否完全不透明渐变的颜色规格 |

在此配方中,聚焦角度设置为零,距离设置为 0.1,中心 X 和 Y 设置为(80,45),半径设置为 120 像素,比例设置为假,循环方法设置为无循环(循环方法。NO_CYCLE),并将两个颜色停止值设置为红色(彩色。红色)和黑色(彩色。黑色)。这些设置通过在(80,45)的中心位置(椭圆的左上方)从红色开始创建径向渐变,然后以 120 像素的距离(半径)将其插值到黑色。

接下来,创建一个具有黄色半透明线性渐变的矩形。黄色矩形使用线性渐变(linear gradient)绘制。

表 14-2 取自为 LinearGradient 类找到的 JavaFX 8 Javadoc 定义(参见docs . Oracle . com/javase/8/Java FX/API/Java FX/scene/paint/linear gradient . html)。

表 14-2。线性梯度属性
|

财产

|

数据类型

|

描述

|
| --- | --- | --- |
| 启动 X | 两倍 | 渐变轴起点的 x 坐标 |
| 起始 Y | 两倍 | 渐变轴起点的 y 坐标 |
| endX(结束 x) | 两倍 | 渐变轴终点的 x 坐标 |
| 周国贤 | 两倍 | 渐变轴终点的 y 坐标 |
| 相称的 | 布尔 | 坐标是否与此渐变填充的形状成比例 |
| 循环法不透明 | 布尔循环方法 | 应用于渐变的循环方法是否该绘画是完全不透明的 |
| 停止 | 列表 | 渐变的颜色规格 |

要创建线性渐变绘画,请指定 startX、startY、endX 和 endY 作为开始点\结束点。起点和终点坐标表示渐变图案的起点和终点。

要创建第二个形状(黄色矩形),请将起点 X 和 Y 设置为(50,50),终点 X 和 Y 设置为(50,75),比例设置为 false,循环方法设置为无循环(cycle method。NO_CYCLE),并将两个颜色停止值设置为黄色(彩色。黄色)和黑色(彩色。黑色),alpha 透明度为 0.784。这些设置从上到下为矩形提供线性渐变,起点为(50,50)(矩形的左上角)。然后,它插值到黑色(矩形的左下方)。

最后,您会注意到一个圆角矩形,在对角线方向上使用绿色和黑色重复渐变图案。这是一个简单的线性渐变绘制,与线性渐变绘制(linear gradient)相同,只是起点 X,Y 和终点 X,Y 设置在对角线位置,循环方法设置为反射(cycle method。反映)。当指定循环方法来反映(cycle method。反射),渐变图案将在颜色之间重复或循环。下面的代码片段实现了具有循环方法 reflect (CycleMethod)的圆角矩形。反映):

LinearGradient cycleGrad = new LinearGradient(50,
                                              50,
                                              70,
                                              70,
                                              false,
                                              CycleMethod.REFLECT,
                                              new Stop(0f, Color.rgb(0, 255, 0, .784)),
                                              new Stop(1.0f, Color.rgb(0, 0, 0, .784)));

14-6.创建菜单

问题

您希望在 JavaFX 应用中创建标准菜单。

解决办法

使用 JavaFX 的菜单控件提供标准化的菜单功能,如复选框菜单、单选菜单、子菜单和分隔符。以下是用于创建菜单的主要类。

javafx.scene.control.MenuBar
javafx.scene.control.Menu
javafx.scene.control.MenuItem

下面的代码调用前面列出的所有菜单功能。该示例代码模拟了一个建筑安全应用,其中包含打开摄像机、发出警报和选择应急计划的菜单选项。

public void start(Stage primaryStage) {
    primaryStage.setTitle("Chapter 14-6 Creating Menus");
    Group root = new Group();
    Scene scene = new Scene(root, 300, 250, Color.WHITE);

    MenuBar menuBar = new MenuBar();

    // File menu - new, save, exit
    Menu menu = new Menu("File");
    menu.getItems().add(new MenuItem("New"));
    menu.getItems().add(new MenuItem("Save"));
    menu.getItems().add(new SeparatorMenuItem());
    menu.getItems().add(new MenuItem("Exit"));

    menuBar.getMenus().add(menu);

    // Cameras menu - camera 1, camera 2
    Menu tools = new Menu("Cameras");
    CheckMenuItem item1 = new CheckMenuItem();
    item1.setText("Show Camera 1");
    item1.setSelected(true);
    tools.getItems().add(item1);

    CheckMenuItem item2 = new CheckMenuItem();
    item2.setText("Show Camera 2");
    item2.setSelected(true);
    tools.getItems().add(item2);

    menuBar.getMenus().add(tools);

    // Alarm
    Menu alarm = new Menu("Alarm");
    ToggleGroup tGroup = new ToggleGroup();

    RadioMenuItem soundAlarmItem = new RadioMenuItem();
    soundAlarmItem.setToggleGroup(tGroup);
    soundAlarmItem.setText("Sound Alarm");

    RadioMenuItem stopAlarmItem = new RadioMenuItem();
    stopAlarmItem.setToggleGroup(tGroup);
    stopAlarmItem.setText("Alarm Off");
    stopAlarmItem.setSelected(true);

    alarm.getItems().add(soundAlarmItem);
    alarm.getItems().add(stopAlarmItem);

    Menu contingencyPlans = new Menu("Contingent Plans");
    contingencyPlans.getItems().add(new CheckMenuItem("Self Destruct in T minus 50"));
    contingencyPlans.getItems().add(new CheckMenuItem("Turn off the coffee machine "));
    contingencyPlans.getItems().add(new CheckMenuItem("Run for your lives! "));

    alarm.getItems().add(contingencyPlans);
    menuBar.getMenus().add(alarm);

    menuBar.prefWidthProperty().bind(primaryStage.widthProperty());

    root.getChildren().add(menuBar);
    primaryStage.setScene(scene);
    primaryStage.show();
}

图 14-7 显示了一个模拟的建筑安全应用,包含检查和子菜单项。

A323910_3_En_14_Fig7_HTML.jpg

图 14-7。创建菜单

它是如何工作的

菜单提供了允许用户从窗口平台应用中选择选项的标准方式。菜单也应该有热键或键盘等价物。用户通常希望使用键盘而不是鼠标来浏览菜单。这个食谱与食谱 14-8 相似,你会注意到许多相似之处。

要创建菜单,首先创建一个 MenuBar 实例,它将包含一对多菜单(MenuItem)对象。创建菜单栏:

MenuBar menuBar = new MenuBar();

第二,创建包含一对多菜单项(menu item)对象的菜单(menu)对象,以及构成子菜单的其他菜单对象。要创建菜单:

Menu menu = new Menu("File");

第三,创建要添加到 menu 对象的菜单项,例如 menu (MenuItem)、check (CheckMenuItem)和 radio 菜单项(RadioMenuItem)。菜单项中可以有图标。我们不会在菜谱中展示这一点,但我们鼓励您探索所有菜单项(MenuItem)的各种构造函数。在创建单选菜单项(radio menu item)时,应该注意 ToggleGroup 类。ToggleGroup 类也用在常规单选按钮(radio buttons)上,以便在任何时候只允许选择一个选项。下面的代码创建要添加到 menu 对象中的单选菜单项(radio menu items):

// Alarm
Menu alarm = new Menu("Alarm");
ToggleGroup tGroup = new ToggleGroup();

RadioMenuItem soundAlarmItem = new RadioMenuItem();
soundAlarmItem.setToggleGroup(tGroup);
soundAlarmItem.setText("Sound Alarm");

RadioMenuItem stopAlarmItem = new RadioMenuItem();
stopAlarmItem.setToggleGroup(tGroup);
stopAlarmItem.setText("Alarm Off");
stopAlarmItem.setSelected(true);

alarm.getItems().add(soundAlarmItem);
alarm.getItems().add(stopAlarmItem);

有时,您可能希望用可视的行分隔符来分隔菜单项。若要创建可视分隔符,请创建一个 SeparatorMenuItem 类的实例,并通过 getItems()方法将其添加到菜单中。getItems()方法返回 MenuItem 对象的可观察列表(ObservableList )。正如你将在食谱 14-10 中看到的,当收藏中的物品被改变时,你会得到通知。下面的代码行向菜单添加了一个可视的行分隔符(SeparatorMenuItem):

menu.getItems().add(new SeparatorMenuItem());

使用的其他菜单项是 check 菜单项(check menu item)和 radio 菜单项(radio menu item),它们分别类似于 JavaFX UI 控件复选框(check box)和单选按钮(radio button)中的对应项。

在将菜单栏添加到场景中之前,您会注意到通过 bind()方法在菜单栏的首选宽度和舞台对象的宽度之间的绑定属性。当绑定这些属性时,当用户调整屏幕大小时,你会看到菜单栏的宽度拉伸。你将在 14-9 中看到绑定是如何工作的。此代码片段显示了菜单栏的 width 属性和舞台的 width 属性之间的绑定。

menuBar.prefWidthProperty().bind(primaryStage.widthProperty());
root.getChildren().add(menuBar);

14-7.向布局添加组件

问题

您希望将 UI 组件添加到类似于网格类型布局的布局中,以便于放置。

解决办法

使用 JavaFX 的 javafx.scene.layout.GridPane 类。此源代码实现了一个简单的 UI 表单,包含名和姓字段控件,并使用网格窗格布局节点(javafx.scene.layout.GridPane):

GridPane gridpane = new GridPane();
gridpane.setPadding(new Insets(5));
gridpane.setHgap(5);
gridpane.setVgap(5);

Label fNameLbl = new Label("First Name");
TextField fNameFld = new TextField();
Label lNameLbl = new Label("First Name");
TextField lNameFld = new TextField();
Button saveButt = new Button("Save");

// First name label
GridPane.setHalignment(fNameLbl, HPos.RIGHT);
gridpane.add(fNameLbl, 0, 0);

// Last name label
GridPane.setHalignment(lNameLbl, HPos.RIGHT);
gridpane.add(lNameLbl, 0, 1);

// First name field
GridPane.setHalignment(fNameFld, HPos.LEFT);       
gridpane.add(fNameFld, 1, 0);

// Last name field
GridPane.setHalignment(lNameFld, HPos.LEFT);
gridpane.add(lNameFld, 1, 1);

// Save button
GridPane.setHalignment(saveButt, HPos.RIGHT);
gridpane.add(saveButt, 1, 2);

root.getChildren().add(gridpane);    

图 14-8 描述了一个包含 UI 控件的小表单,它使用了一个网格窗格布局节点。

A323910_3_En_14_Fig8_HTML.jpg

图 14-8。向布局添加控件

它是如何工作的

构建用户界面的最大挑战之一是如何将控件放置在显示区域。在开发 GUI 应用时,最理想的是应用允许用户移动和调整其可视区域的大小,同时保持愉快的用户体验。与 Java Swing 类似,JavaFX layout 有一些常用的布局,它们提供了在场景图上显示 UI 控件的最常见方式。这个菜谱演示了 GridPane 类。

回想一下菜谱 14-4,其中您实现了一个自定义布局,以类似网格的方式显示组件。您可能会注意到相似之处,但是我们遗漏了许多实现特性,比如调整最小/最大尺寸、填充和垂直对齐。令人惊讶的是,JavaFX 团队已经创建了一个健壮的类似网格的布局,称为 GridPane。

首先创建一个 GridPane 的实例。接下来,使用插入对象的实例设置填充。设置填充后,只需设置水平和垂直间距。以下代码片段实例化了一个网格窗格(grid pane),其填充、水平和垂直间距设置为 5(像素):

GridPane gridpane = new GridPane();
gridpane.setPadding(new Insets(5));
gridpane.setHgap(5);
gridpane.setVgap(5);

填充是区域内容周围的顶部、右侧、底部和左侧间距,以像素为单位。当获得首选大小时,填充将包括在计算中。设置水平和垂直间距与单元格内 UI 控件之间的间距有关。

接下来,只需将每个 UI 控件放入各自的单元格位置。所有单元格都是零相对的。以下代码片段将“保存”按钮 UI 控件添加到单元格(1,2)处的网格窗格布局节点(grid pane)中:

gridpane.add(saveButt, 1, 2);

该布局还允许您在单元格中水平或垂直对齐控件。以下代码语句将“保存”按钮右对齐:

GridPane.setHalignment(saveButt, HPos.RIGHT);

14-8.生成边框

问题

您想要创建和自定义图像周围的边框。

解决办法

使用 JavaFX 的 CSS 样式 API 创建一个应用来动态定制边框区域。

下面的代码创建一个应用,该应用具有一个 CSS 编辑器文本区域和一个围绕图像的边框视图区域。默认情况下,编辑器的文本区域将包含 JavaFX 样式选择器,这些选择器在图像周围创建一条蓝色虚线。您将有机会在 CSS 编辑器中通过单击 Bling 来修改样式选择器的值!按钮来应用边框设置。

primaryStage.setTitle("Chapter 14-8 Generating Borders");
Group root = new Group();
Scene scene = new Scene(root, 600, 330, Color.WHITE);

// create a grid pane
GridPane gridpane = new GridPane();
gridpane.setPadding(new Insets(5));
gridpane.setHgap(10);
gridpane.setVgap(10);

// label CSS Editor
Label cssEditorLbl = new Label("CSS Editor");
GridPane.setHalignment(cssEditorLbl, HPos.CENTER);
gridpane.add(cssEditorLbl, 0, 0);

// label Border View
Label borderLbl = new Label("Border View");
GridPane.setHalignment(borderLbl, HPos.CENTER);
gridpane.add(borderLbl, 1, 0);

// Text area for CSS editor
final TextArea cssEditorFld = new TextArea();
cssEditorFld.setPrefRowCount(10);
cssEditorFld.setPrefColumnCount(100);
cssEditorFld.setWrapText(true);
cssEditorFld.setPrefWidth(150);
GridPane.setHalignment(cssEditorFld, HPos.CENTER);
gridpane.add(cssEditorFld, 0, 1);

String cssDefault = "-fx-border-color: blue;\n"
        + "-fx-border-insets: 5;\n"
        + "-fx-border-width: 3;\n"
        + "-fx-border-style: dashed;\n";

cssEditorFld.setText(cssDefault);

// Border decorate the picture
final ImageView imv = new ImageView();
final Image image2 = new Image(GeneratingBorders.class.getResourceAsStream("smoke_glass_buttons1.png"));
imv.setImage(image2);

final HBox pictureRegion = new HBox();
pictureRegion.setStyle(cssDefault);
pictureRegion.getChildren().add(imv);
gridpane.add(pictureRegion, 1, 1);

Button apply = new Button("Bling!");
GridPane.setHalignment(apply, HPos.RIGHT);
gridpane.add(apply, 0, 2);

apply.setOnAction((e) -> {
    pictureRegion.setStyle(cssEditorFld.getText());
});

root.getChildren().add(gridpane);
primaryStage.setScene(scene);
primaryStage.show();

图 14-9 展示了边框定制应用。

A323910_3_En_14_Fig9_HTML.jpg

图 14-9。生成边框

它是如何工作的

JavaFX 能够设计 JavaFX 节点的样式,类似于 web 开发领域中的 CSS(也在 Recipe 14-3 中演示)。这种强大的 API 可以改变节点的背景颜色、字体、边框和许多其他属性,本质上允许开发人员或设计人员使用 CSS 为 GUI 控件换肤。

这个解决方案允许用户在左边的文本区域输入 JavaFX CSS 样式。按钮,在右边显示的图像周围应用样式。根据节点的类型,可以设置的样式是有限制的。要查看所有样式选择器的完整列表,请参考 JavaFX CSS 参考指南:docs . Oracle . com/Java se/8/Java FX/API/Java FX/scene/doc-files/CSS ref . html

在应用 JavaFX CSS 样式的第一步中,您必须确定要样式化的节点类型。在各种节点类型上设置属性时,您会发现某些节点有局限性。在这个配方中,目的是在 ImageView 对象周围放置一个边框。因为 ImageView 不是从 Region 扩展的,所以它不包含边框样式属性。因此,要解决这个问题,只需创建一个 HBox 布局来包含 imageView,并对 HBox 应用 JavaFX CSS。以下代码使用 setStyle()方法将 JavaFX CSS 边框样式应用于水平框区域(HBox ):

String cssDefault = "-fx-border-color: blue;\n"
     + "-fx-border-insets: 5;\n"
     + "-fx-border-width: 3;\n"
 + "-fx-border-style: dashed;\n";
final ImageView imv = new ImageView();
...//
final HBox pictureRegion = new HBox();
**pictureRegion.setStyle(cssDefault);** 
pictureRegion.getChildren().add(imv);

14-9.绑定表达式

问题

您希望同步两个值之间的更改。

解决办法

使用 javafx.beans.binding.和 javafx.beans.property.包绑定变量。绑定值或属性时,需要考虑多个场景。该配方演示了以下三种绑定策略:

  • Java Bean 上的双向绑定

  • 使用 Fluent API 的高级绑定

  • 使用 javafx.beans.binding.* binding 对象的低级绑定

以下代码是实现这三种策略的控制台应用。控制台应用将基于各种绑定场景输出属性值。第一个场景是字符串属性变量和域对象(Contact)拥有的字符串属性(如 firstName 属性)之间的双向绑定。下一个场景是使用 fluent 接口 API 计算矩形面积的高级绑定。最后一个场景是使用低级绑定策略来计算球体的体积。高级绑定和低级绑定的区别在于,高级绑定使用 multiply()和 subtract()等方法,而不是运算符和-。使用低级绑定时,使用派生的 NumberBinding 类,如 DoubleBinding 类。使用 DoubleBinding 类,您可以覆盖它的 computeValue()方法,这样您就可以使用熟悉的运算符,如和-来表达复杂的数学方程:

package org.java9recipes.chapter14.recipe14_09;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 * Recipe 14-9: Binding Expressions
 * @author cdea
 * Update:  J. Juneau
 */
public class BindingExpressions {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        System.out.println("Chapter 14-9 Binding Expressions\n");

        System.out.println("Binding a Contact bean [Bi-directional binding]");
        Contact contact = new Contact("John", "Doe");
        StringProperty fname = new SimpleStringProperty();
        fname.bindBidirectional(contact.firstNameProperty());
        StringProperty lname = new SimpleStringProperty();
        lname.bindBidirectional(contact.lastNameProperty());

        System.out.println("Current - StringProperty values   : " + fname.getValue() + " " + lname.getValue());
        System.out.println("Current - Contact values          : " + contact.getFirstName() + " " + contact.getLastName());

        System.out.println("Modifying StringProperty values");
        fname.setValue("Jane");
        lname.setValue("Deer");

        System.out.println("After - StringProperty values   : " + fname.getValue() + " " + lname.getValue());
        System.out.println("After - Contact values          : " + contact.getFirstName() + " " + contact.getLastName());

        System.out.println();
        System.out.println("A Area of a Rectangle [High level Fluent API]");

        // Area = width * height
        final IntegerProperty width = new SimpleIntegerProperty(10);
        final IntegerProperty height = new SimpleIntegerProperty(10);

        NumberBinding area = width.multiply(height);

        System.out.println("Current - Width and Height     : " + width.get() + " " + height.get());
        System.out.println("Current - Area of the Rectangle: " + area.getValue());
        System.out.println("Modifying width and height");

        width.set(100);
        height.set(700);

        System.out.println("After - Width and Height     : " + width.get() + " " + height.get());
        System.out.println("After - Area of the Rectangle: " + area.getValue());

        System.out.println();
        System.out.println("A Volume of a Sphere [low level API]");

        // volume = 4/3 * pi r³
        final DoubleProperty radius = new SimpleDoubleProperty(2);

        DoubleBinding volumeOfSphere = new DoubleBinding() {
            {
                super.bind(radius);
            }

            @Override
            protected double computeValue() {
                return (4 / 3 * Math.PI * Math.pow(radius.get(), 3));
            }
        };

        System.out.println("Current - radius for Sphere: " + radius.get());
        System.out.println("Current - volume for Sphere: " + volumeOfSphere.get());
        System.out.println("Modifying DoubleProperty radius");

        radius.set(50);
        System.out.println("After - radius for Sphere: " + radius.get());
        System.out.println("After - volume for Sphere: " + volumeOfSphere.get());

    }
}

class Contact {

    private SimpleStringProperty firstName = new SimpleStringProperty();
    private SimpleStringProperty lastName = new SimpleStringProperty();

    public Contact(String fn, String ln) {
        firstName.setValue(fn);
        lastName.setValue(ln);
    }

    public final String getFirstName() {
        return firstName.getValue();
    }

    public StringProperty firstNameProperty() {
        return firstName;
    }

    public final void setFirstName(String firstName) {
        this.firstName.setValue(firstName);
    }

    public final String getLastName() {
        return lastName.getValue();
    }

    public StringProperty lastNameProperty() {
        return lastName;
    }

    public final void setLastName(String lastName) {
        this.lastName.setValue(lastName);
    }
}

以下输出演示了三种绑定方案:

Binding a Contact bean [Bi-directional binding]
Current - StringProperty values   : John Doe
Current - Contact values          : John Doe
Modifying StringProperty values
After - StringProperty values   : Jane Deer
After - Contact values          : Jane Deer

A Area of a Rectangle [High level Fluent API]
Current - Width and Height     : 10 10
Current - Area of the Rectangle: 100
Modifying width and height
After - Width and Height     : 100 700
After - Area of the Rectangle: 70000

A Volume of a Sphere [low level API]
Current - radius for Sphere: 2.0
Current - volume for Sphere: 25.132741228718345
Modifying DoubleProperty radius
After - radius for Sphere: 50.0
After - volume for Sphere: 392699.0816987241

它是如何工作的

绑定意味着至少两个值正在同步。这意味着当一个因变量改变时,另一个变量也会改变。JavaFX 提供了许多绑定选项,使开发人员能够同步域对象和 GUI 控件中的属性。这个方法演示了三种常见的绑定场景。

绑定变量最简单的方法之一是使用双向绑定。当域对象包含将要绑定到 GUI 表单的数据时,通常会使用这种场景。这个菜谱创建了一个简单的 contact (Contact)对象,其中包含名和姓。注意使用 SimpleStringProperty 类的实例变量。许多以 Property 结尾的类都是 javafx.beans.Observable 类,它们都可以被绑定。为了绑定这些属性,它们必须是相同的数据类型。在前面的示例中,您在创建的联系人域对象之外创建了 SimpleStringProperty 类型的 first name 和 last name 变量。一旦创建了它们,就可以双向绑定它们,以允许在任一端进行更新。因此,如果您更改域对象,其他绑定属性也会更新。当外部变量被修改时,域对象的属性被更新。下面演示了针对域对象(联系人)上的字符串属性的双向绑定:

Contact contact = new Contact("John", "Doe");
StringProperty fname = new SimpleStringProperty();
fname.bindBidirectional(contact.firstNameProperty());
StringProperty lname = new SimpleStringProperty();
lname.bindBidirectional(contact.lastNameProperty());

接下来是如何绑定数字。使用 Fluent API 时,绑定数字很简单。这种高级机制允许开发人员使用简单的算术将变量绑定到计算值。基本上,一个公式“必然”会根据它所绑定的变量的变化来改变它的结果。查看 Javadoc(docs . Oracle . com/javase/8/Java FX/API/Java FX/beans/binding/bindings . html)了解所有可用方法和数字类型的详细信息。在本例中,您只需创建一个矩形面积的公式。area (NumberBinding)是绑定,其依赖项是 width 和 height (IntegerProperty)属性。当使用 fluent 接口 API 进行绑定时,您会注意到 multiply()方法。根据 Javadoc,所有属性类都继承自 NumberExpressionBase 类,该类包含基于数字的流畅接口 API。以下代码片段使用了 fluent 接口 API:

// Area = width * height
final IntegerProperty width = new SimpleIntegerProperty(10);
final IntegerProperty height = new SimpleIntegerProperty(10);    
NumberBinding area = width.multiply(height);

关于绑定号码的最后一个场景被认为是一种更低级的方法。这允许开发者使用原语和更复杂的数学运算。这里,您使用一个 DoubleBinding 类来求解给定半径的球体的体积。首先实现 computeValue()方法来计算体积。显示了通过覆盖 computeValue()方法来计算球体体积的低级绑定场景:

final DoubleProperty radius = new SimpleDoubleProperty(2);

DoubleBinding volumeOfSphere = new DoubleBinding() {
        {
            super.bind(radius);
        }

        @Override
        protected double computeValue() {
                return (4 / 3 * Math.PI * Math.pow(radius.get(), 3));
        }
};

14-10.创建和使用可观察列表

问题

您希望创建一个包含两个列表视图控件的 GUI 应用,允许用户在两个列表之间传递项目。

解决办法

您可以利用 JavaFX 的 Java FX . collections . observable list 和 javafx.scene.control.ListView 类来提供一种模型-视图-控制器(MVC)机制,每当操作后端列表时,该机制都会更新 UI 的列表视图控件。

下面的代码创建了一个包含两个列表的 GUI 应用,允许用户将一个列表中包含的项目发送给另一个列表。在这里,您将创建一个虚构的应用来挑选被认为是英雄的候选人。用户从左边的列表中挑选潜在的候选人,将他们移动到右边的列表中作为英雄。这演示了 UI 列表控件(ListView)与后端存储列表(ObservableList)同步的能力。

public void start(Stage primaryStage) {
    primaryStage.setTitle("Chapter 14-10 Creating and Working with ObservableLists");
    Group root = new Group();
    Scene scene = new Scene(root, 400, 250, Color.WHITE);

    // create a grid pane
    GridPane gridpane = new GridPane();
    gridpane.setPadding(new Insets(5));
    gridpane.setHgap(10);
    gridpane.setVgap(10);

    // candidates label
    Label candidatesLbl = new Label("Candidates");
    GridPane.setHalignment(candidatesLbl, HPos.CENTER);
    gridpane.add(candidatesLbl, 0, 0);

    Label heroesLbl = new Label("Heroes");
    gridpane.add(heroesLbl, 2, 0);
    GridPane.setHalignment(heroesLbl, HPos.CENTER);

    // candidates
    final ObservableList<String> candidates = FXCollections.observableArrayList("Super man",
            "Spider man",
            "Wolverine",
            "Police",
            "Fire Rescue",
            "Soldiers",
            "Dad & Mom",
            "Doctor",
            "Politician",
            "Pastor",
            "Teacher");
    final ListView<String> candidatesListView = new ListView<>(candidates);
    candidatesListView.setPrefWidth(150);
    candidatesListView.setPrefHeight(150);

    gridpane.add(candidatesListView, 0, 1);

    // heros
    final ObservableList<String> heroes = FXCollections.observableArrayList();
    final ListView<String> heroListView = new ListView<>(heroes);
    heroListView.setPrefWidth(150);
    heroListView.setPrefHeight(150);

    gridpane.add(heroListView, 2, 1);

    // select heroes
    Button sendRightButton = new Button(">");
    sendRightButton.setOnAction((e) -> {
            String potential = candidatesListView.getSelectionModel().getSelectedItem();
            if (potential != null) {
                candidatesListView.getSelectionModel().clearSelection();
                candidates.remove(potential);
                heroes.add(potential);
            }       
    });

    // deselect heroes
    Button sendLeftButton = new Button("<");
    sendLeftButton.setOnAction((e) -> {
            String notHero = heroListView.getSelectionModel().getSelectedItem();
            if (notHero != null) {
                heroListView.getSelectionModel().clearSelection();
                heroes.remove(notHero);
                candidates.add(notHero);
            }
    });

    VBox vbox = new VBox(5);
    vbox.getChildren().addAll(sendRightButton,sendLeftButton);

    gridpane.add(vbox, 1, 1);
    GridPane.setConstraints(vbox, 1, 1, 1, 2,HPos.CENTER, VPos.CENTER);

    root.getChildren().add(gridpane);        
    primaryStage.setScene(scene);
    primaryStage.show();
}

图 14-10 描绘了英雄选择应用。

A323910_3_En_14_Fig10_HTML.jpg

图 14-10。列表视图和可观察列表

它是如何工作的

当处理 Java 集合时,你会注意到有这么多有用的容器类,它们代表了所有类型的数据结构。一个常用的集合是 java.util. ArrayList 类。当使用包含 ArrayList 的域对象构建应用时,开发人员可以轻松地操作集合中的对象。但是,在过去(过去),将 Java Swing 组件与集合结合使用是一个挑战,尤其是更新 GUI 以反映域对象的变化。你如何解决这个问题?好吧,JavaFX 是值得关注的救星!

说到营救,这个菜谱演示了一个 GUI 应用,允许用户选择他们喜欢的英雄。这与通过在列表框组件中添加或删除项目来管理用户角色的应用屏幕非常相似。在 JavaFX 中,使用 ListView 控件保存字符串对象。在创建 ListView 的实例之前,会创建包含候选对象的 ObservableList。在本例中,您会注意到使用了一个名为 FXCollections 的工厂类,在该类中,您可以传递要包装的公共集合类型,并将其作为 ObservableList 返回给调用者。这个食谱传入的是一个字符串数组,而不是一个 ArrayList,所以希望您了解如何使用 FXCollections 类。一定要明智地使用它:“权力越大,责任也越大。”此代码行调用 FXCollections 类返回一个可观察列表(observable list):

ObservableList<String> candidates = FXCollections.observableArrayList(...);

创建 ObservableList 后,ListView 类使用接收 observable list 的构造函数进行实例化。这里显示的是创建和填充 ListView 对象的代码:

ListView<String> candidatesListView = new ListView<String>(candidates);

在最后一项业务中,代码将像操作 java.util.ArrayLists 一样操作 ObservableList,一旦被操作,ListView 将被通知并自动更新以反映 observable list 的变化。以下代码片段在用户按下“向右发送”按钮时实现事件处理程序和操作事件:

// select heroes
Button sendRightButton = new Button(">");
sendRightButton.setOnAction((e) -> {
        String potential = candidatesListView.getSelectionModel().getSelectedItem();
        if (potential != null) {
            candidatesListView.getSelectionModel().clearSelection();
            candidates.remove(potential);
            heroes.add(potential);
        }       
});

设置动作时,通过 lambda 表达式实现 EventHandler 来监听按钮按下事件。当按钮按下事件到达时,代码将确定 ListView 中的哪一项被选中。一旦项目被确定,你清除选择,删除项目,并将项目添加到英雄的 ObservableList。

14-11.生成后台进程

问题

您希望创建一个 GUI 应用,在向用户显示进度的同时,使用后台处理来模拟长时间运行的流程。

解决办法

创建一个典型的对话框应用,在后台复制文件时显示进度指示器。以下是该配方中使用的主要类别:

  • Java FX . scene . control . progress bar

  • Java FX . scene . control . progressive indicator

  • javafx.concurrent.Task 类

下面的源代码是一个模拟文件复制对话框的应用,该对话框显示进度指示器并执行后台进程:

package org.java9recipes.chapter14.recipe14_11;

import java.util.Random;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class BackgroundProcesses extends Application {

    static Task copyWorker;
    final int numFiles = 30;

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Chapter 14-11 Background Processes");
        Group root = new Group();
        Scene scene = new Scene(root, 330, 120, Color.WHITE);

        BorderPane mainPane = new BorderPane();
        mainPane.layoutXProperty().bind(scene.widthProperty().subtract(mainPane.widthProperty()).divide(2));
        root.getChildren().add(mainPane);

        final Label label = new Label("Files Transfer:");
        final ProgressBar progressBar = new ProgressBar(0);
        final ProgressIndicator progressIndicator = new ProgressIndicator(0);

        final HBox hb = new HBox();
        hb.setSpacing(5);
        hb.setAlignment(Pos.CENTER);
        hb.getChildren().addAll(label, progressBar, progressIndicator);
        mainPane.setTop(hb);

        final Button startButton = new Button("Start");
        final Button cancelButton = new Button("Cancel");
        final TextArea textArea = new TextArea();
        textArea.setEditable(false);
        textArea.setPrefSize(200, 70);
        final HBox hb2 = new HBox();
        hb2.setSpacing(5);
        hb2.setAlignment(Pos.CENTER);
        hb2.getChildren().addAll(startButton, cancelButton, textArea);
        mainPane.setBottom(hb2);

        // wire up start button
        startButton.setOnAction((e) -> {
            startButton.setDisable(true);
            progressBar.setProgress(0);
            progressIndicator.setProgress(0);
            textArea.setText("");
            cancelButton.setDisable(false);
            copyWorker = createWorker(numFiles);

            // wire up progress bar
            progressBar.progressProperty().unbind();
            progressBar.progressProperty().bind(copyWorker.progressProperty());
            progressIndicator.progressProperty().unbind();
            progressIndicator.progressProperty().bind(copyWorker.progressProperty());

            // append to text area box
            copyWorker.messageProperty().addListener(new ChangeListener<String>() {

                public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                    textArea.appendText(newValue + "\n");
                }
            });

            new Thread(copyWorker).start();
        });

        // cancel button will kill worker and reset.
        cancelButton.setOnAction((e) -> {
            startButton.setDisable(false);
            cancelButton.setDisable(true);
            copyWorker.cancel(true);

            // reset
            progressBar.progressProperty().unbind();
            progressBar.setProgress(0);
            progressIndicator.progressProperty().unbind();
            progressIndicator.setProgress(0);
            textArea.appendText("File transfer was cancelled.");
        });

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public Task createWorker(final int numFiles) {
        return new Task() {

            @Override
            protected Object call() throws Exception {
                for (int i = 0; i < numFiles; i++) {
                    long elapsedTime = System.currentTimeMillis();
                    copyFile("some file", "some dest file");
                    elapsedTime = System.currentTimeMillis() - elapsedTime;
                    String status = elapsedTime + " milliseconds";

                    // queue up status
                    updateMessage(status);
                    updateProgress(i + 1, numFiles);
                }
                return true;
            }
        };
    }

    public void copyFile(String src, String dest) throws InterruptedException {
        // simulate a long time
        Random rnd = new Random(System.currentTimeMillis());
        long millis = rnd.nextInt(1000);
        Thread.sleep(millis);
    }
}

图 14-11 显示了后台进程应用,它模拟了一个文件复制窗口。

A323910_3_En_14_Fig11_HTML.jpg

图 14-11。后台进程

它是如何工作的

GUI 开发的主要陷阱之一是知道何时以及如何委派工作(线程)。您会不断地被提醒线程安全,尤其是在阻塞 GUI 线程的时候。当使用 Java Swing API 时,必须实现 SwingWorker 对象来推迟 EDT 的非 GUI 工作。类似的模式和原则仍然适用于 JavaFX 世界。

您首先创建两个而不是一个进度控件,向用户显示正在完成的工作。一个是进度条,另一个是进度指示器。进度指示器在指示器图标下方显示一个百分比。以下代码片段显示了进度控件的初始创建:

final ProgressBar progressBar = new ProgressBar(0);
final ProgressIndicator progressIndicator = new ProgressIndicator(0);

接下来,通过 createWorker()方法创建一个工作线程。createWorker()便利方法将实例化并返回一个 javafx.concurrent.Task 对象,该对象类似于 Java Swing 的 SwingWorker 类。与 SwingWorker 类不同,Task 对象被大大简化,更易于使用。如果您比较最后一个配方,您会注意到没有一个 GUI 控件被传递到任务中。聪明的 JavaFX 团队已经创建了允许您绑定的可观察属性。这促进了一种更加事件驱动的方法来处理工作(任务)。创建 Task 对象的实例时,实现 call()方法在后台执行工作。在工作过程中,您可能希望将中间结果排队,如进度或文本信息。为此,您可以调用 updateProgress()和 updateMessage()方法。这些方法将以线程安全的方式更新信息,以便进度属性的观察者能够安全地更新 GUI,而不会阻塞 GUI 线程。以下代码片段演示了对消息和进度进行排队的能力:

// queue up status
updateMessage(status);
updateProgress(i + 1, numFiles);

创建工作任务后,取消绑定到进度控件的任何旧任务。一旦进度控件被解除绑定,您就可以将进度控件绑定到新创建的名为 copyWorker 的任务对象。此处显示的是用于将新任务对象重新绑定到进度 UI 控件的代码:

// wire up progress bar
progressBar.progressProperty().unbind();
progressBar.progressProperty().bind(copyWorker.progressProperty());
progressIndicator.progressProperty().unbind();
progressIndicator.progressProperty().bind(copyWorker.progressProperty());

接下来,实现 ChangeListener 以将排队的结果追加到 TextArea 控件中。JavaFX 属性的另一个显著特点是,您可以附加许多类似于 Java Swing 组件的侦听器。最后,工作器和控件都连接起来,产生一个线程在后台运行。以下代码行显示了如何启动任务工作器对象:

new Thread(copyWorker).start();

最后,cancel 按钮将简单地调用任务对象的 Cancel()方法来终止进程。一旦任务被取消,进度控制将被重置。一旦工作者任务被取消,它就不能被重用。按下时,开始按钮会重新创建一个新任务。如果您想要一个更健壮的解决方案,您应该看看 javafx.concurrent.Service 类。下面的代码行将取消一个任务工作对象:

copyWorker.cancel(true);

14-12.将键盘序列与应用相关联

问题

您希望为菜单选项创建键盘快捷键。

解决办法

创建一个使用 JavaFX 组合键 API 的应用。您将使用的主要类如下所示:

  • javafx.scene.input.KeyCode

  • Java FX . scene . input . key code combination

  • Java FX . scene . input . key combination

下面的源代码清单是一个显示绑定到菜单项的可用键盘快捷键的应用。当用户执行键盘快捷键时,应用将在屏幕上显示组合键:

public void start(Stage primaryStage) {
    primaryStage.setTitle("Chapter 14-12 Associating Keyboard Sequences");
    Group root = new Group();
    Scene scene = new Scene(root, 530, 300, Color.WHITE);

    final StringProperty statusProperty = new SimpleStringProperty();

    InnerShadow iShadow = new InnerShadow();
    iShadow.setOffsetX(3.5f);
    iShadow.setOffsetY(3.5f);

    final Text status = new Text();
    status.setEffect(iShadow);
    status.setX(100);
    status.setY(50);
    status.setFill(Color.LIME);
    status.setFont(Font.font(null, FontWeight.BOLD, 35));
    status.setTranslateY(50);

    status.textProperty().bind(statusProperty);
    statusProperty.set("Keyboard Shortcuts \nCtrl-N, \nCtrl-S, \nCtrl-X");
    root.getChildren().add(status);

    MenuBar menuBar = new MenuBar();
    menuBar.prefWidthProperty().bind(primaryStage.widthProperty());
    root.getChildren().add(menuBar);

    Menu menu = new Menu("File");
    menuBar.getMenus().add(menu);

    MenuItem newItem = new MenuItem();
    newItem.setText("New");
    newItem.setAccelerator(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN));
    newItem.setOnAction((e) -> {
        statusProperty.set("Ctrl-N");
    });
    menu.getItems().add(newItem);

    MenuItem saveItem = new MenuItem();
    saveItem.setText("Save");
    saveItem.setAccelerator(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN));
    saveItem.setOnAction((e) -> {
        statusProperty.set("Ctrl-S");
    });
    menu.getItems().add(saveItem);

    menu.getItems().add(new SeparatorMenuItem());

    MenuItem exitItem = new MenuItem();
    exitItem.setText("Exit");
    exitItem.setAccelerator(new KeyCodeCombination(KeyCode.X, KeyCombination.CONTROL_DOWN));
    exitItem.setOnAction((e) -> {
        statusProperty.set("Ctrl-X");
    });
    menu.getItems().add(exitItem);

    primaryStage.setScene(scene);
    primaryStage.show();
}

图 14-12 显示了一个演示键盘快捷键的应用。

A323910_3_En_14_Fig12_HTML.jpg

图 14-12。键盘序列/快捷键

它是如何工作的

这个菜谱的解决方案演示了如何使用 Java FX . scene . input . key code combination 和 Java FX . scene . input . key combination 类创建组合键或键盘快捷键。看到之前的食谱有点无聊,我们决定让这里的东西更有趣一点。当用户执行组合键时,这个配方在场景图上显示文本节点。当显示文本节点时,我们应用了内部阴影效果。以下代码片段创建了一个带有内部阴影效果的文本节点:

InnerShadow iShadow = new InnerShadow();
iShadow.setOffsetX(3.5f);
iShadow.setOffsetY(3.5f);

final Text status = new Text();
status.setEffect(iShadow);
status.setX(100);
status.setY(50);
status.setFill(Color.LIME);
status.setFont(Font.font(null, FontWeight.BOLD, 35));
status.setTranslateY(50);

要创建键盘快捷键,只需调用菜单或按钮控件的 setAccelerator()方法。在这个配方中,组合键是使用 MenuItem 节点的 setAccelerator()方法设置的。下面的代码行指定了 Ctrl-N 的组合键:

MenuItem newItem = new MenuItem();
newItem.setText("New");
newItem.setAccelerator(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN));
newItem.setOnAction((e) -> {
    statusProperty.set("Ctrl-N");
});

从代码中可以看出,在示例中,当按下加速器(组合键)时,会触发 onAction ActionEvent。它通过 lambda 表达式将 statusProperty 值设置为 Ctrl-N。

14-13.创建和使用表格

问题

您希望在类似于 Java Swing 的 JTable 组件的 UI 表格控件中显示项目。

解决办法

使用 JavaFX 的 javafx.scene.control.TableView 类创建应用。TableView 控件提供了与 Swing 的 JTable 组件等效的功能。

为了练习 TableView 控件,您将创建一个显示老板和雇员的应用。在左边,您将实现一个包含老板的 ListView 控件,雇员(下属)将显示在右边的 TableView 控件中。

此处显示的是一个简单的 domain (Person)类的源代码,该类表示要在 ListView 或 TableView 控件中显示的老板或雇员:

package org.java9recipes.chapter15.recipe15_14;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class Person {

    private StringProperty aliasName;
    private StringProperty firstName;
    private StringProperty lastName;
    private ObservableList<Person> employees = FXCollections.observableArrayList();

    public final void setAliasName(String value) {
        aliasNameProperty().set(value);
    }

    public final String getAliasName() {
        return aliasNameProperty().get();
    }

    public StringProperty aliasNameProperty() {
        if (aliasName == null) {
            aliasName = new SimpleStringProperty();
        }
        return aliasName;
    }

    public final void setFirstName(String value) {
        firstNameProperty().set(value);
    }

    public final String getFirstName() {
        return firstNameProperty().get();
    }

    public StringProperty firstNameProperty() {
        if (firstName == null) {
            firstName = new SimpleStringProperty();
        }
        return firstName;
    }

    public final void setLastName(String value) {
        lastNameProperty().set(value);
    }

    public final String getLastName() {
        return lastNameProperty().get();
    }

    public StringProperty lastNameProperty() {
        if (lastName == null) {
            lastName = new SimpleStringProperty();
        }
        return lastName;
    }

    public ObservableList<Person> employeesProperty() {
        return employees;
    }

    public Person(String alias, String firstName, String lastName) {
        setAliasName(alias);
        setFirstName(firstName);
        setLastName(lastName);
    }

}

下面是主要的应用代码。它在左侧显示一个包含老板的列表视图组件,在右侧显示一个包含雇员的表视图控件:

public void start(Stage primaryStage) {
    primaryStage.setTitle("Chapter 14-13 Working with Tables");
    Group root = new Group();
    Scene scene = new Scene(root, 500, 250, Color.WHITE);

    // create a grid pane
    GridPane gridpane = new GridPane();
    gridpane.setPadding(new Insets(5));
    gridpane.setHgap(10);
    gridpane.setVgap(10);

    // candidates label
    Label candidatesLbl = new Label("Boss");
    GridPane.setHalignment(candidatesLbl, HPos.CENTER);
    gridpane.add(candidatesLbl, 0, 0);

    // List of leaders
    ObservableList<Person> leaders = getPeople();
    final ListView<Person> leaderListView = new ListView<>(leaders);
    leaderListView.setPrefWidth(150);
    leaderListView.setPrefHeight(150);

    // display first and last name with tooltip using alias
    leaderListView.setCellFactory((ListView<Person> param) -> {
        final Label leadLbl = new Label();
        final Tooltip tooltip = new Tooltip();
        final ListCell<Person> cell = new ListCell<Person>() {
            @Override
            public void updateItem(Person item, boolean empty) {
                super.updateItem(item, empty);
                if (item != null) {
                    leadLbl.setText(item.getAliasName());
                    setText(item.getFirstName() + " " + item.getLastName());
                    tooltip.setText(item.getAliasName());
                    setTooltip(tooltip);
                }
            }
        }; // ListCell
        return cell;
    }); // setCellFactory

    gridpane.add(leaderListView, 0, 1);

    Label emplLbl = new Label("Employees");
    gridpane.add(emplLbl, 2, 0);
    GridPane.setHalignment(emplLbl, HPos.CENTER);

    final TableView<Person> employeeTableView = new TableView<>();
    employeeTableView.setPrefWidth(300);

    final ObservableList<Person> teamMembers = FXCollections.observableArrayList();
    employeeTableView.setItems(teamMembers);

    TableColumn<Person, String> aliasNameCol = new TableColumn<>("Alias");
    aliasNameCol.setEditable(true);
    aliasNameCol.setCellValueFactory(new PropertyValueFactory("aliasName"));

    aliasNameCol.setPrefWidth(employeeTableView.getPrefWidth() / 3);

    TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
    firstNameCol.setCellValueFactory(new PropertyValueFactory("firstName"));
    firstNameCol.setPrefWidth(employeeTableView.getPrefWidth() / 3);

    TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
    lastNameCol.setCellValueFactory(new PropertyValueFactory("lastName"));
    lastNameCol.setPrefWidth(employeeTableView.getPrefWidth() / 3);

    employeeTableView.getColumns().setAll(aliasNameCol, firstNameCol, lastNameCol);
    gridpane.add(employeeTableView, 2, 1);

    // selection listening
    leaderListView.getSelectionModel().selectedItemProperty().addListener(
            (ObservableValue<? extends Person> observable, Person oldValue, Person newValue) -> {
        if (observable != null && observable.getValue() != null) {
            teamMembers.clear();
            teamMembers.addAll(observable.getValue().employeesProperty());
        }
    });

    root.getChildren().add(gridpane);

    primaryStage.setScene(scene);
    primaryStage.show();
}

以下代码显示了 WorkingWithTables 主应用类中包含的 getPeople()方法。此方法填充前面显示的 UI TableView 控件:

    private ObservableList<Person> getPeople() {
        ObservableList<Person> people = FXCollections.<Person>observableArrayList();
        Person docX = new Person("Professor X", "Charles", "Xavier");
        docX.employeesProperty().add(new Person("Wolverine", "James", "Howlett"));
        docX.employeesProperty().add(new Person("Cyclops", "Scott", "Summers"));
        docX.employeesProperty().add(new Person("Storm", "Ororo", "Munroe"));

        Person magneto = new Person("Magneto", "Max", "Eisenhardt");
        magneto.employeesProperty().add(new Person("Juggernaut", "Cain", "Marko"));
        magneto.employeesProperty().add(new Person("Mystique", "Raven", "Darkhölme"));
        magneto.employeesProperty().add(new Person("Sabretooth", "Victor", "Creed"));

        Person biker = new Person("Mountain Biker", "Jonathan", "Gennick");
        biker.employeesProperty().add(new Person("JavaJuneau", "Joshua", "Juneau"));
        biker.employeesProperty().add(new Person("Freddy", "Freddy", "Guime"));
        biker.employeesProperty().add(new Person("Mark", "Mark", "Beaty"));
        biker.employeesProperty().add(new Person("John", "John", "O'Conner"));
        biker.employeesProperty().add(new Person("D-Man", "Carl", "Dea"));

        people.add(docX);
        people.add(magneto);
        people.add(biker);

        return people;
    }

图 14-13 显示了演示 JavaFX 的 TableView 控件的应用。

A323910_3_En_14_Fig13_HTML.jpg

图 14-13。使用表格

它是如何工作的

为了好玩,我们创建了一个简单的 GUI 来显示员工和他们的老板。你会注意到在图 14-13 的左边是一个人员列表(老板)。当用户选择一个老板时,他们的员工将显示在右边的表视图区域中。当您将鼠标悬停在所选老板上方时,您还会注意到工具提示。

在考虑 TableView 控件之前,理解负责更新 TableView 的 ListView 很重要。在模型视图方式中,创建了一个 ObservableList,其中包含 ListView 控件的构造函数的所有 bosses。这个代码称老板为领导者。下面的代码创建一个 ListView 控件:

// List of leaders
ObservableList<Person> leaders = getPeople();
final ListView<Person> leaderListView = new ListView<Person>(leaders);

接下来,创建一个单元格工厂,以便在 ListView 控件中正确显示人名。因为每一项都是一个 Person 对象,所以 ListView 不知道如何呈现 ListView 控件中的每一行。您只需通过指定 ListView 和 ListCell 数据类型来创建 javafx.util.Callback 通用类型对象。如果您使用的是 NetBeans 之类的可信 IDE,它会预先生成实现方法调用()之类的东西。接下来是 ListCell 类型的变量单元格(在 call()方法中),在其中创建一个 lambda 表达式。lambda 表达式包含 updateItem()方法的实现。若要实现 updateItem()方法,请获取人员信息并更新 Label 控件(leadLbl)。最后,将工具提示设置为相关的文本。

然后创建一个 TableView 控件,根据从 ListView 中选择的老板显示雇员。创建 TableView 时,首先创建列标题。使用以下代码创建一个表列:

TableColumn<String> firstNameCol = new TableColumn<String>("First Name");
**firstNameCol.setProperty("firstName");** 

一旦创建了一个列,您会注意到 setProperty()方法,它负责调用 person Bean 的属性。当雇员列表被放入 TableView 中时,它将知道如何提取属性以放入表中的每个单元格中。

最后是 JavaFX 中 ListViewer 上的选择监听器的实现,称为选择项属性(selection item property)。创建并添加一个 ChangeListener 来侦听选择事件。当用户选择一个老板时,TableView 被清除并填充了该老板的雇员。实际上是 ObservableList 的魔力通知了 TableView 的变化。要通过 teamMembers (ObservableList)变量填充 TableView,请使用以下代码:

teamMembers.clear();
teamMembers.addAll(observable.getValue().employeesProperty());

14-14.用拆分视图组织用户界面

问题

您希望使用拆分分割线控件来拆分 GUI 屏幕。

解决办法

使用 JavaFX 的分割窗格控件。javafx.scene.control.SplitPane 类是一个 UI 控件,使您能够将屏幕划分为类似框架的区域。split 控件允许用户用鼠标在任意两个拆分区域之间移动分隔线。

此处显示的代码用于创建利用 javafx.scene.control.SplitPane 类的 GUI 应用。该类将屏幕分成三个窗口区域。三个窗口区域是左列、右上区域和右下区域。此外,文本节点被添加到这三个区域。

public void start(Stage primaryStage) {
    primaryStage.setTitle("Chapter 14-4 Organizing UI with Split Views");
    Group root = new Group();
    Scene scene = new Scene(root, 350, 250, Color.WHITE);

    // Left and right split pane
    SplitPane splitPane = new SplitPane();
    splitPane.prefWidthProperty().bind(scene.widthProperty());
    splitPane.prefHeightProperty().bind(scene.heightProperty());

    //List<Node> items = splitPane.getItems();
    VBox leftArea = new VBox(10);

    for (int i = 0; i < 5; i++) {
        HBox rowBox = new HBox(20);
        final Text leftText = new Text();
        leftText.setText("Left " + i);
        leftText.setTranslateX(20);
        leftText.setFill(Color.BLUE);
        leftText.setFont(Font.font(null, FontWeight.BOLD, 20));

        rowBox.getChildren().add(leftText);
        leftArea.getChildren().add(rowBox);
    }
    leftArea.setAlignment(Pos.CENTER);

    // Upper and lower split pane
    SplitPane splitPane2 = new SplitPane();
    splitPane2.setOrientation(Orientation.VERTICAL);
    splitPane2.prefWidthProperty().bind(scene.widthProperty());
    splitPane2.prefHeightProperty().bind(scene.heightProperty());

    HBox centerArea = new HBox();

    InnerShadow iShadow = new InnerShadow();
    iShadow.setOffsetX(3.5f);
    iShadow.setOffsetY(3.5f);

    final Text upperRight = new Text();
    upperRight.setText("Upper Right");
    upperRight.setX(100);
    upperRight.setY(50);
    upperRight.setEffect(iShadow);
    upperRight.setFill(Color.LIME);
    upperRight.setFont(Font.font(null, FontWeight.BOLD, 35));
    upperRight.setTranslateY(50);
    centerArea.getChildren().add(upperRight);

    HBox rightArea = new HBox();

    final Text lowerRight = new Text();
    lowerRight.setText("Lower Right");
    lowerRight.setX(100);
    lowerRight.setY(50);
    lowerRight.setEffect(iShadow);
    lowerRight.setFill(Color.RED);
    lowerRight.setFont(Font.font(null, FontWeight.BOLD, 35));
    lowerRight.setTranslateY(50);
    rightArea.getChildren().add(lowerRight);

    splitPane2.getItems().add(centerArea);
    splitPane2.getItems().add(rightArea);

    // add left area
    splitPane.getItems().add(leftArea);

    // add right area
    splitPane.getItems().add(splitPane2);

    // evenly position divider
    ObservableList<SplitPane.Divider> dividers = splitPane.getDividers();
    for (int i = 0; i < dividers.size(); i++) {
        dividers.get(i).setPosition((i + 1.0) / 3);
    }

    HBox hbox = new HBox();
    hbox.getChildren().add(splitPane);
    root.getChildren().add(hbox);

    primaryStage.setScene(scene);
    primaryStage.show();
}

图 14-14 描述了使用分割窗格控件的应用。

A323910_3_En_14_Fig14_HTML.jpg

图 14-14。拆分视图

它是如何工作的

如果你曾经见过一个简单的富站点摘要(RSS)阅读器或 Javadocs,你会注意到屏幕被分隔线分成了几个部分。这个食谱创建了三个区域:左边、右上和右下。

首先创建一个 SplitPane,将场景的左右区域分开。然后将它的宽度和高度属性绑定到场景,这样当用户调整舞台大小时,这些区域将占用可用空间。接下来,创建一个表示左侧区域的 VBox 布局控件。在 VBox (leftArea)中,循环生成一系列文本节点。接下来,生成分割窗格的右侧。以下代码片段允许拆分窗格控件(split pane)水平拆分:

SplitPane splitPane = new SplitPane();
splitPane.prefWidthProperty().bind(scene.widthProperty());
splitPane.prefHeightProperty().bind(scene.heightProperty());

现在您创建 SplitPane 来垂直划分区域,这将形成右上和右下区域。此处显示的是用于垂直分割窗口区域的代码:

// Upper and lower split pane
SplitPane splitPane2 = new SplitPane();
splitPane2.setOrientation(Orientation.VERTICAL);

最后,您组装分割的窗格,并调整分隔线的位置,以便平均分配屏幕空间。以下代码组装拆分窗格,并遍历分隔线列表以更新它们的位置:

splitPane.getItems().add(splitPane2);

// evenly position divider
ObservableList<SplitPane.Divider> dividers = splitPane.getDividers();
for (int i = 0; i < dividers.size(); i++) {
    dividers.get(i).setPosition((i + 1.0) / 3);
}

HBox hbox = new HBox();
hbox.getChildren().add(splitPane);
root.getChildren().add(hbox);

14-15.向用户界面添加选项卡

问题

您希望创建一个带有选项卡的 GUI 应用。

解决办法

使用 JavaFX 的选项卡和选项卡窗格控件。选项卡(javafx.scene.control.Tab)和选项卡窗格控件(javafx.scene.control.TabPane)类允许您在各个选项卡中放置图形节点。

下面的代码示例创建一个简单的应用,该应用具有允许用户选择 tab 键方向的菜单选项。可用的选项卡方向有顶部、底部、左侧和右侧。

public void start(Stage primaryStage) {
    primaryStage.setTitle("Chapter 14-15 Adding Tabs to a UI");
    Group root = new Group();
    Scene scene = new Scene(root, 400, 250, Color.WHITE);

    TabPane tabPane = new TabPane();

    MenuBar menuBar = new MenuBar();

    EventHandler<ActionEvent> action = changeTabPlacement(tabPane);

    Menu menu = new Menu("Tab Side");
    MenuItem left = new MenuItem("Left");

    left.setOnAction(action);
    menu.getItems().add(left);

    MenuItem right = new MenuItem("Right");
    right.setOnAction(action);
    menu.getItems().add(right);

    MenuItem top = new MenuItem("Top");
    top.setOnAction(action);
    menu.getItems().add(top);

    MenuItem bottom = new MenuItem("Bottom");
    bottom.setOnAction(action);
    menu.getItems().add(bottom);

    menuBar.getMenus().add(menu);

    BorderPane borderPane = new BorderPane();

    // generate 10 tabs
    for (int i = 0; i < 10; i++) {
        Tab tab = new Tab();
        tab.setText("Tab" + i);
        HBox hbox = new HBox();
        hbox.getChildren().add(new Label("Tab" + i));
        hbox.setAlignment(Pos.CENTER);
        tab.setContent(hbox);
        tabPane.getTabs().add(tab);
    }

    // add tab pane
    borderPane.setCenter(tabPane);

    // bind to take available space
    borderPane.prefHeightProperty().bind(scene.heightProperty());
    borderPane.prefWidthProperty().bind(scene.widthProperty());

    // add menu bar
    borderPane.setTop(menuBar);

    // add border Pane
    root.getChildren().add(borderPane);

    primaryStage.setScene(scene);
    primaryStage.show();
}

private EventHandler<ActionEvent> changeTabPlacement(final TabPane tabPane) {
    return (ActionEvent event) -> {
        MenuItem mItem = (MenuItem) event.getSource();
        String side = mItem.getText();
        if ("left".equalsIgnoreCase(side)) {
            tabPane.setSide(Side.LEFT);
        } else if ("right".equalsIgnoreCase(side)) {
            tabPane.setSide(Side.RIGHT);
        } else if ("top".equalsIgnoreCase(side)) {
            tabPane.setSide(Side.TOP);
        } else if ("bottom".equalsIgnoreCase(side)) {
            tabPane.setSide(Side.BOTTOM);
        }
    };
}

图 14-15 显示标签应用,允许用户改变标签方向。

A323910_3_En_14_Fig15_HTML.jpg

图 14-15。塔帕布

它是如何工作的

使用 TabPane 控件时,您可能已经知道希望选项卡显示的方向。这个应用允许你通过左、右、上、下菜单选项来设置方向。

如果您熟悉 Swing API,您可能会注意到 JavaFX TabPane 与 Swing JTabbedPanel 非常相似。您只需添加 javafx.scene.control.Tab 实例,而不是添加 JPanels。下面的代码片段将选项卡控件添加到 TabPane 控件中:

TabPane tabPane = new TabPane();
Tab tab = new Tab();
tab.setText("Tab" + i);
tabPane.getTabs().add(tab);

当您更改 TabPane 控件的方向时,请使用 setSide()方法。下面的代码设置 TabPane 控件的方向:

tabPane.setSide(Side.BOTTOM);

在这个配方中,一个菜单用于改变 TabPane 控件的方向。不同的方向被分配给菜单的不同 MenuItem 节点,一个标识为 changeTabPlacement 的 EventHandler 用于在选择不同的 MenuItem 时更改方向。EventHandler 只是检查 MenuItem 的文本,以确定应该将哪个方向应用于 TabPane。

14-16.开发一个对话框

问题

您希望创建一个包含对话框的应用,该对话框包含一些供用户输入的文本字段。

解决办法

使用 JavaFX 的 stage (javafx.stage.Stage)和 scene(Java FX . scene . scene)API 创建对话框。

下面的源代码清单是一个模拟更改密码对话框的应用。该应用包含弹出对话框的菜单选项。除了使用菜单选项,用户还可以设置对话框的模态状态(modality)。

public class DevelopingADialog extends Application {

    static Stage LOGIN_DIALOG;
    static int dx = 1;
    static int dy = 1;

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    private static Stage createLoginDialog(Stage parent, boolean modal) {
        if (LOGIN_DIALOG != null) {
            LOGIN_DIALOG.close();
        }
        return new MyDialog(parent, modal, "Welcome to JavaFX!");
    }

    @Override
    public void start(final Stage primaryStage) {
        primaryStage.setTitle("Chapter 14-16 Developing a Dialog");
        Group root = new Group();
        Scene scene = new Scene(root, 433, 312, Color.WHITE);

        MenuBar menuBar = new MenuBar();
        menuBar.prefWidthProperty().bind(primaryStage.widthProperty());

        Menu menu = new Menu("Home");

        // add change password menu itme
        MenuItem newItem = new MenuItem("Change Password", null);
        newItem.setOnAction((ActionEvent event) -> {
            if (LOGIN_DIALOG == null) {
                LOGIN_DIALOG = createLoginDialog(primaryStage, true);
            }
            LOGIN_DIALOG.sizeToScene();
            LOGIN_DIALOG.show();
        });

        menu.getItems().add(newItem);

        // add separator
        menu.getItems().add(new SeparatorMenuItem());

        // add non modal menu item
        ToggleGroup modalGroup = new ToggleGroup();
        RadioMenuItem nonModalItem = new RadioMenuItem();
        nonModalItem.setToggleGroup(modalGroup);
        nonModalItem.setText("Non Modal");
        nonModalItem.setSelected(true);

        nonModalItem.setOnAction((ActionEvent event) -> {
            LOGIN_DIALOG = createLoginDialog(primaryStage, false);
        });

        menu.getItems().add(nonModalItem);

        // add modal selection
        RadioMenuItem modalItem = new RadioMenuItem();
        modalItem.setToggleGroup(modalGroup);
        modalItem.setText("Modal");
        modalItem.setSelected(true);

        modalItem.setOnAction((ActionEvent event) -> {
            LOGIN_DIALOG = createLoginDialog(primaryStage, true);
        });
        menu.getItems().add(modalItem);

        // add separator
        menu.getItems().add(new SeparatorMenuItem());

        // add exit
        MenuItem exitItem = new MenuItem("Exit", null);
        exitItem.setMnemonicParsing(true);
        exitItem.setAccelerator(new KeyCodeCombination(KeyCode.X, KeyCombination.CONTROL_DOWN));
        exitItem.setOnAction((ActionEvent event) -> {
            Platform.exit();
        });
        menu.getItems().add(exitItem);

        // add menu
        menuBar.getMenus().add(menu);

        // menu bar to window
        root.getChildren().add(menuBar);

        primaryStage.setScene(scene);
        primaryStage.show();

        addBouncyBall(scene);
    }

    private void addBouncyBall(final Scene scene) {

        final Circle ball = new Circle(100, 100, 20);
        RadialGradient gradient1 = new RadialGradient(0,
                .1,
                100,
                100,
                20,
                false,
                CycleMethod.NO_CYCLE,
                new Stop(0, Color.RED),
                new Stop(1, Color.BLACK));

        ball.setFill(gradient1);

        final Group root = (Group) scene.getRoot();
        root.getChildren().add(ball);

        Timeline tl = new Timeline();
        tl.setCycleCount(Animation.INDEFINITE);
        KeyFrame moveBall = new KeyFrame(Duration.seconds(.0200), (ActionEvent event) -> {
            double xMin = ball.getBoundsInParent().getMinX();
            double yMin = ball.getBoundsInParent().getMinY();
            double xMax = ball.getBoundsInParent().getMaxX();
            double yMax = ball.getBoundsInParent().getMaxY();

            // Collision - boundaries
            if (xMin < 0 || xMax > scene.getWidth()) {
                dx = dx * -1;
            }
            if (yMin < 0 || yMax > scene.getHeight()) {
                dy = dy * -1;
            }

            ball.setTranslateX(ball.getTranslateX() + dx);
            ball.setTranslateY(ball.getTranslateY() + dy);
        });

        tl.getKeyFrames().add(moveBall);
        tl.play();
    }
}

class MyDialog extends Stage {

    public MyDialog(Stage owner, boolean modality, String title) {
        super();
        initOwner(owner);
        Modality m = modality ? Modality.APPLICATION_MODAL : Modality.NONE;
        initModality(m);
        setOpacity(.90);
        setTitle(title);
        Group root = new Group();
        Scene scene = new Scene(root, 250, 150, Color.WHITE);
        setScene(scene);

        GridPane gridpane = new GridPane();
        gridpane.setPadding(new Insets(5));
        gridpane.setHgap(5);
        gridpane.setVgap(5);

        Label mainLabel = new Label("Enter User Name & Password");
        gridpane.add(mainLabel, 1, 0, 2, 1);

        Label userNameLbl = new Label("User Name: ");
        gridpane.add(userNameLbl, 0, 1);

        Label passwordLbl = new Label("Password: ");
        gridpane.add(passwordLbl, 0, 2);

        // username text field
        final TextField userNameFld = new TextField("Admin");
        gridpane.add(userNameFld, 1, 1);

        // password field
        final PasswordField passwordFld = new PasswordField();
        passwordFld.setText("drowssap");
        gridpane.add(passwordFld, 1, 2);

        Button login = new Button("Change");
        login.setOnAction((ActionEvent event) -> {
            close();
        });
        gridpane.add(login, 1, 3);
        GridPane.setHalignment(login, HPos.RIGHT);
        root.getChildren().add(gridpane);
    }
}

图 14-16 描述了启用非模态选项的更改密码对话框应用。

A323910_3_En_14_Fig16_HTML.jpg

图 14-16。开发一个对话框

它是如何工作的

为了创建对话框,JavaFX 使用 javafx.stage.Stage 类的另一个实例向用户显示。类似于从 Swing 中的 JDialog 类扩展,您只需从 Stage 类扩展即可。您有机会在构造函数中传递所属窗口,然后该构造函数调用 initOwner()方法。可以使用 initModality()方法设置对话框的模式状态。下面的类从 Stage 类扩展而来,具有初始化所属阶段和模式状态的构造函数:

class MyDialog extends Stage {

    public MyDialog(Stage owner, boolean modality, String title) {
        super();
        initOwner(owner);
        Modality m = modality ? Modality.APPLICATION_MODAL : Modality.NONE;
initModality(m);

        ...// The rest of the class

剩下的代码创建一个场景(scene),类似于主应用的 start()方法。因为登录表单非常无聊,我们决定在用户忙于在对话框中更改密码时创建一个弹跳球的动画。(你会在以后的食谱中看到更多关于创建动画的内容。)

当选择“更改密码”菜单项时,createLoginDialog 方法检查是否已经有一个 MyDialog 实例被实例化。如果是这样,它将关闭该实例并生成一个新实例。然后显示新创建的对话框。同样,RadioMenuItem 控件调用 createLoginDialog 方法,传递不同的布尔值来指示实例化的 MyDialog 实例是否应设置为 modal。如前所述,弹力球对对话框没有影响;只是为了效果而加的。

14-17.使用 JavaFX 打印

问题

您希望提供在应用场景图中打印指定节点的能力。

解决办法

利用 JavaFX 打印 API 打印指定的节点,并构建复杂的打印对话框。在这个解决方案中,生成了一个用于绘图的 JavaFX 应用。绘图应用允许您通过打印按钮打印画布。调用打印按钮时,会打开一个对话框,提供打印选项,如打印机和布局选择。

以下代码用于构建应用阶段,包括所有按钮和绘图功能。第一个类不包含任何打印逻辑…您将在接下来看到…并且这些源被显示出来是为了使它易于跟随示例。

public class PrintingWithJavaFX extends Application {

    static Stage PRINT_DIALOG;

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(PrintingWithJavaFX.class, args);
    }

    private static Stage createPrintDialog(Stage parent, boolean modal, Canvas node) {
        if (PRINT_DIALOG != null) {
            PRINT_DIALOG.close();
        }
        // Copy canvas
        WritableImage wim = new WritableImage(300, 300);
        node.snapshot(null, wim);
        ImageView iv = new ImageView();
        iv.setImage(wim);
        return new PrintDialog(parent, modal, "Printing Menu", iv);
    }

    @Override
    public void start(Stage primaryStage) {

        StackPane root = new StackPane();
        Canvas canvas = new Canvas(300, 300);
        final GraphicsContext graphicsContext = canvas.getGraphicsContext2D();

        final Button printButton = new Button("Print");
        final BooleanProperty printingProperty = new SimpleBooleanProperty(false);
        printButton.setOnAction(actionEvent-> {
           printingProperty.set(true);
           if (PRINT_DIALOG == null) {
                PRINT_DIALOG = createPrintDialog(primaryStage, true, canvas);
            }
            PRINT_DIALOG.sizeToScene();
            PRINT_DIALOG.show();
        });
        printButton.setTranslateX(3);

        final Button resetButton = new Button("Reset");
        resetButton.setOnAction(actionEvent-> {
            graphicsContext.clearRect(1, 1,
                    graphicsContext.getCanvas().getWidth()-2,
                    graphicsContext.getCanvas().getHeight()-2);
        });
        resetButton.setTranslateX(10);

        // Set up the pen color chooser
        ChoiceBox colorChooser = new ChoiceBox(FXCollections.observableArrayList(
            "Black", "Blue", "Red", "Green", "Brown", "Orange")
        );
        // Select the first option by default
        colorChooser.getSelectionModel().selectFirst();

        colorChooser.getSelectionModel().selectedIndexProperty().addListener(
                (ChangeListener)(ov, old, newval) -> {
                        Number idx = (Number)newval;
                        Color newColor;
                        switch(idx.intValue()){
                            case 0: newColor = Color.BLACK;
                                    break;
                            case 1: newColor = Color.BLUE;
                                    break;
                            case 2: newColor = Color.RED;
                                    break;
                            case 3: newColor = Color.GREEN;
                                    break;
                            case 4: newColor = Color.BROWN;
                                    break;
                            case 5: newColor = Color.ORANGE;
                                    break;
                            default: newColor = Color.BLACK;
                                    break;
                        }
                        graphicsContext.setStroke(newColor);

                });
        colorChooser.setTranslateX(5);

        ChoiceBox sizeChooser = new ChoiceBox(FXCollections.observableArrayList(
            "1", "2", "3", "4", "5")
        );
        // Select the first option by default
        sizeChooser.getSelectionModel().selectFirst();

        sizeChooser.getSelectionModel().selectedIndexProperty().addListener(
                (ChangeListener)(ov, old, newval) -> {
                        Number idx = (Number)newval;

                        switch(idx.intValue()){
                            case 0: graphicsContext.setLineWidth(1);
                                    break;
                            case 1: graphicsContext.setLineWidth(2);
                                    break;
                            case 2: graphicsContext.setLineWidth(3);
                                    break;
                            case 3: graphicsContext.setLineWidth(4);
                                    break;
                            case 4: graphicsContext.setLineWidth(5);
                                    break;
                            default: graphicsContext.setLineWidth(1);
                                    break;
                        }
                });
        sizeChooser.setTranslateX(5);

        canvas.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent event) -> {
            graphicsContext.beginPath();
            graphicsContext.moveTo(event.getX(), event.getY());
            graphicsContext.stroke();
        });

        canvas.addEventHandler(MouseEvent.MOUSE_DRAGGED, (MouseEvent event) -> {
            graphicsContext.lineTo(event.getX(), event.getY());
            graphicsContext.stroke();
        });

        canvas.addEventHandler(MouseEvent.MOUSE_RELEASED, (MouseEvent event) -> {
        });

        HBox buttonBox = new HBox();
        buttonBox.getChildren().addAll(printButton, colorChooser, sizeChooser, resetButton);

        initDraw(graphicsContext, canvas.getLayoutX(), canvas.getLayoutY());

        BorderPane container = new BorderPane();
        container.setTop(buttonBox);

        container.setCenter(canvas);

        root.getChildren().add(container);
        Scene scene = new Scene(root, 400, 400);
        primaryStage.setTitle("Recipe 14-17:  Printing from JavaFX");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void initDraw(GraphicsContext gc, double x, double y){
        double canvasWidth = gc.getCanvas().getWidth();
        double canvasHeight = gc.getCanvas().getHeight();

        gc.fill();
        gc.strokeRect(
                x,              //x of the upper left corner
                y,              //y of the upper left corner
                canvasWidth,    //width of the rectangle
                canvasHeight);  //height of the rectangle

        //gc.setFill(Color.RED);
        //gc.setStroke(Color.BLUE);
        //gc.setLineWidth(1);

    }

}

接下来,您将查看创建 PrintDialog 类的源代码,该类包含应用的所有打印逻辑。使用时按下打印按钮,对话框打开。它包含一些使用 JavaFX Print API 的节点。

class PrintDialog extends Stage {

    public PrintDialog(Stage owner, boolean modality, String title, Node printNode) {
        super();
        initOwner(owner);
        Modality m = modality ? Modality.APPLICATION_MODAL : Modality.NONE;
        initModality(m);
        setOpacity(.90);
        setTitle(title);
        Group root = new Group();
        Scene scene = new Scene(root, 450, 150, Color.WHITE);
        setScene(scene);

        GridPane gridpane = new GridPane();
        gridpane.setPadding(new Insets(5));
        gridpane.setHgap(5);
        gridpane.setVgap(5);

        Label printerLabel = new Label("Printer: ");
        gridpane.add(printerLabel, 0, 1);

        Label layoutLabel = new Label("Layout: ");
        gridpane.add(layoutLabel, 0, 2);

        final Printer selectedPrinter = Printer.getDefaultPrinter();
        // printer pick list
        ChoiceBox printerChooser = new ChoiceBox(FXCollections.observableArrayList(
            Printer.getAllPrinters())
        );
        // Select the first option by default
        printerChooser.getSelectionModel().selectFirst();

        gridpane.add(printerChooser, 1, 1);

        ChoiceBox layoutChooser = new ChoiceBox(FXCollections.observableArrayList(
            "Portait", "Landscape")
        );
        layoutChooser.getSelectionModel().selectFirst();

        layoutChooser.getSelectionModel().selectedIndexProperty().addListener(
                (ChangeListener)(ov, old, newval) -> {
                        Number idx = (Number)newval;
                        switch(idx.intValue()){
                            case 0: selectedPrinter.createPageLayout(Paper.A0, PageOrientation.PORTRAIT, Printer.MarginType.EQUAL);
                                    break;
                            case 1: selectedPrinter.createPageLayout(Paper.A0, PageOrientation.LANDSCAPE, Printer.MarginType.EQUAL);
                                    break;

                            default: selectedPrinter.createPageLayout(Paper.A0, PageOrientation.PORTRAIT, Printer.MarginType.EQUAL);
                                    break;
                        }
                });
        gridpane.add(layoutChooser,1,2);
        Button printButton = new Button("Print");
        printButton.setOnAction((ActionEvent event) -> {
            print(printNode, selectedPrinter);
        });
        gridpane.add(printButton, 0, 3);

        GridPane.setHalignment(printButton, HPos.RIGHT);
        root.getChildren().add(gridpane);
    }

    public void print(final Node node, Printer printer) {

        PrinterJob job = PrinterJob.createPrinterJob();
        job.setPrinter(printer);
        if (job != null) {
            boolean success = job.printPage(node);
            if (success) {
                job.endJob();
            }
        }
    }    
}

图 14-17 显示了该应用。使用对话框打印画布内的区域(绘图区域)(参见图 14-18 )。

A323910_3_En_14_Fig17_HTML.jpg

图 14-17。具有打印功能的 JavaFX 绘图应用

A323910_3_En_14_Fig18_HTML.jpg

图 14-18。利用 JavaFX 打印 API 打印菜单

它是如何工作的

在 JavaFX 8 之前的 JavaFX 版本中,没有用于打印部分应用阶段的标准 API。在 JavaFX 8 中,添加了一个 Print API 来标准化处理打印特性的方式。该 API 还使得使用很少的代码就可以轻松地启用具有打印功能的应用。这个 API 非常大,因为它包含了许多类,但是它非常简单易用。

要启用指定节点的打印功能,首先要使用 javafx.print.PrinterJob 类,因为它包含了生成非常简单的打印任务的所有功能。要将节点发送到默认的系统打印机,只需调用 PrintJob.createPrinterJob()来返回 PrinterJob 对象。返回对象后,检查以确保它不为空,然后调用它的 printPage()方法,传递要打印的节点。包含此功能的解决方案摘录显示在以下代码行中:

public void print(final Node node, Printer printer) {

    PrinterJob job = PrinterJob.createPrinterJob()
    job.setPrinter(printer);
    if (job != null) {
        boolean success = job.printPage(node);
        if (success) {
            job.endJob();
        }
    }
  }    

虽然只需要使用 PrinterJob 就可以将节点发送到打印机,但是 API 允许进行更多的定制。表 14-3 列出了 API 中可用的不同类,并简要描述了它们的功能。

表 14-3。JavaFX 打印 API
|

类别名

|

描述

|
| --- | --- |
| 作业设置 | 封装打印作业的设置 |
| 页面布局 | 封装布局设置 |
| 打印范围 | 用于选择范围或限制打印页面 |
| 纸 | 封装打印机的纸张尺寸 |
| 纸张来源 | 用于纸张的输入托盘 |
| 打印机 | 代表打印作业的目的地 |
| 打印机属性 | 封装打印机的属性 |
| 打印机作业 | 用于调用 JavaFX 场景图形打印 |
| 打印分辨率 | 表示支持的设备分辨率 |

在该示例中,生成了一个打印对话框,允许用户选择将打印作业发送到哪里。它还提供了选择所需打印布局(纵向或横向)的控件。打印机。可以调用 getDefaultPrinter()方法来返回主机的默认打印机。在该示例中,通过调用 Printer.getAllPrinters()方法,ChoiceBox 用于显示主机上可用的所有打印机。然后,在 print 方法内的 PrinterJob 上设置所选的打印机,该方法将所需的节点发送到该打印机。

通过另一个选择框选择打印机布局,选择布局时,所选打印机的选项会更新。下面一行代码演示如何将布局设置为 PageOrientation。所选印刷品的肖像:

selectedPrinter.createPageLayout(Paper.A0, PageOrientation.PORTRAIT, Printer.MarginType.EQUAL);

任何节点都可以发送到 PrinterJob,但发送要打印的节点的副本很重要,因为打印任务可能会修改该节点。

Print API 很大,但是很容易理解。这个方法只是触及了 API 的一些皮毛。一旦准备好开发自己的打印机流程,我们建议您通读 Javadoc 以获得更多详细信息。然而,这个食谱应该提供一个如何开始的基本理解。请参见 Javadoc 的以下链接:docs . Oracle . com/javase/8/Java FX/API/Java FX/print/package-summary . html

14-18.在 JavaFX 中嵌入 Swing 内容

问题

您希望将一些简单的 Java Swing 内容嵌入到 JavaFX 应用中。

解决办法

创建一个 JavaFX 应用,并使用 SwingNode 类将 Swing 内容嵌入其中。在下面的示例中,一个简单的 JavaFX 应用用于在基于 Swing 的用户输入表单和基于 JavaFX 的表单之间切换。应用中的 JavaFX 按钮可以用来确定当用户单击它时应该显示哪个表单。

首先,让我们看看嵌入到 JavaFX 应用中的 Swing 表单的代码。代码存在于一个名为 SwingForm.java 的类中。

import java.awt.GridLayout;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class SwingForm extends JPanel {

    JLabel formTitle, first, last, buttonLbl;
    protected JTextField firstField, lastField;

    public SwingForm(){

    JPanel innerPanel = new JPanel();

    GridLayout gl = new GridLayout(3,2);
    innerPanel.setLayout(gl);

    first = new JLabel("First Name:");
    innerPanel.add(first);
    firstField = new JTextField(10);
    innerPanel.add(firstField);

    last = new JLabel("Last Name:");
    innerPanel.add(last);
    lastField = new JTextField(10);
    innerPanel.add(lastField);

    JButton button = new JButton("Submit");
    button.addActionListener((event) -> {
        Platform.runLater(()-> {
            UserEntryForm.fxLabel.setText("Message from Swing form...");
        });
    });
    buttonLbl = new JLabel("Click Me:");
    innerPanel.add(buttonLbl);
    innerPanel.add(button);
    add(innerPanel);

    }
}

接下来,让我们看看用于创建图形用户界面的 JavaFX 代码,包括切换按钮和 JavaFX 表单。注意,Swing 表单是使用 SwingNode 对象嵌入的。

public class UserEntryForm extends Application {

    private static ToggleButton fxbutton;
    private static GridPane grid;
    public static Label fxLabel;

    @Override
    public void start(Stage stage) {
        final SwingNode swingNode = new SwingNode();
        createSwingContent(swingNode);
        BorderPane pane = new BorderPane();
        Image fxButtonIcon = new Image(
                getClass().getResourceAsStream("img/duke1.gif"));
        String buttonText = "Use Swing Form";
        fxbutton = new ToggleButton(buttonText, new ImageView(fxButtonIcon));
        fxbutton.setTooltip(
                new Tooltip("This button chooses between the Swing and FX form"));
        fxbutton.setStyle("-fx-font: 22 arial; -fx-base: #cce6ff;");
        fxbutton.setAlignment(Pos.CENTER);
        fxbutton.setOnAction((event)->{
            ToggleButton toggle = (ToggleButton) event.getSource();
            if(!toggle.isSelected()){
                swingNode.setDisable(true);
                swingNode.setVisible(false);
                grid.setDisable(false);
                grid.setVisible(true);
                fxbutton.setText("Use Swing Form");
            } else {
                swingNode.setDisable(false);
                swingNode.setVisible(true);
                grid.setDisable(true);
                grid.setVisible(false);
                fxbutton.setText("Use JavaFX Form");
            }
        });
        // Disable SwingNode by default
        swingNode.setVisible(false);
        Text appTitle = new Text("Swing/FX Form Demo");
        appTitle.setFont(Font.font("Tahoma", FontWeight.NORMAL, 20));

        pane.setTop(appTitle);
        HBox formPanel = new HBox();
        formPanel.setSpacing(10);
        fxLabel = new Label("Message from JavaFX form...");

        formPanel.getChildren().addAll(fxFormContent(), swingNode);

        pane.setCenter(formPanel);
        VBox vbox = new VBox();
        vbox.getChildren().addAll(fxbutton, fxLabel);

        pane.setBottom(vbox);

        Scene scene = new Scene(pane, 700, 500);
        stage.setScene(scene);
        stage.setTitle("Swing Form Embedded In JavaFX");
        stage.show();
    }

    private void createSwingContent(final SwingNode swingNode) {
        SwingUtilities.invokeLater(() -> {
            swingNode.setContent(new SwingForm());
        });
    }

    private GridPane fxFormContent() {
        grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(25, 25, 25, 25));

        Text scenetitle = new Text("Enter User");
        scenetitle.setFont(Font.font("Tahoma", FontWeight.NORMAL, 20));
        grid.add(scenetitle, 0, 0, 2, 1);

        Label first = new Label("First Name:");
        grid.add(first, 0, 1);

        TextField firstField = new TextField();
        grid.add(firstField, 1, 1);

        Label last = new Label("Last Name:");
        grid.add(last, 0, 2);

        TextField lastField = new TextField();
        grid.add(lastField, 1, 2);

        Button messageButton = new Button("Click");
        messageButton.setOnAction((event) ->{
            fxLabel.setText("Message from JavaFX Form...");
        });
        grid.add(messageButton, 0,3);

        return grid;

    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

调用时,应用看起来如图 14-19 所示。

A323910_3_En_14_Fig19_HTML.jpg

图 14-19。使用 SwingNode 嵌入一个 Swing 表单

它是如何工作的

有大量的应用是使用 Java Swing 框架编写的。有时在 JavaFX 应用中使用这些应用是有意义的,或者在有意义的地方嵌入那些 Swing 应用的一部分。javafx.embed.swing.SwingNode 类通过将 JComponent 传递给 SwingNode setContent()方法,可以轻松地将 JComponent 实例嵌入到 javafx 应用中。内容被自动重画,所有事件都被转发到 JComponent 实例,无需用户干预。

在这个菜谱的例子中,通过实例化一个新的 SwingNode 对象并向其传递 Swing form 类的实例,嵌入了一个简单的 Java Swing 表单。Swing 内容应该在 EDT 上运行,因此任何 Swing 访问都应该在 EDT 上进行。也就是说,使用 SwingUtilities.invokeLater 创建了一个新线程,lambda 表达式封装了用于设置 Swing 内容的 Runnable。

也可以在 Swing 代码中与 JavaFX 内容进行交互。为此,您必须在 JavaFX 应用线程中运行 JavaFX 代码,方法是调用 javafx.application.Platform 类并调用 runLater()方法,同时传递一个 Runnable。例如,在示例代码中,Swing 表单中的按钮可以使用下面的代码回调 JavaFX 标签来更改文本。请注意,JavaFX 标签是一个公共字段,因此可以从 Swing 类中直接访问它。

JButton button = new JButton("Submit");
    button.addActionListener((event) -> {
        Platform.runLater(()-> {
            UserEntryForm.fxLabel.setText("Message from Swing form...");
        });
    });
注意

默认情况下,JavaFX 应用线程和 Swing EDT 是分开的。EDT 不运行 Swing 应用的 GUI 代码。然而,在 JavaFX 中,平台 GUI 线程运行应用代码。有一个启用单线程模式的实验设置,它允许 JavaFX 平台 GUI 线程在 Swing 和 JavaFX 一起使用时成为 EDT。要启用实验设置,请使用以下选项执行您的代码:djavafx . embed . single thread = true

通过利用 JavaFX 8 的新特性,您可以生成一个包含嵌入式 Swing 代码的 JavaFX 应用,该代码可以直接与 JavaFX 代码进行通信。

摘要

JavaFX 是 Java Swing API 的继承者。它使开发人员能够为下一代应用开发复杂而强大的用户界面。本章为您提供了对 JavaFX 的基本理解,以及一些最广泛使用的 JavaFX APIs。在接下来几章的课程中,您将学习更多关于 JavaFX 的知识,比如如何构造 3D 对象和 WebViews。

十五、JavaFX 图形

你听过有人说“当两个世界相撞”吗?当一个来自不同背景或文化的人被放在一个他们意见相左并且必须面对非常困难的决定的情况下,这个表达就会被使用。当我们构建一个需要动画的 GUI 应用时,我们经常处于商业和游戏世界的冲突之中。

在富客户端应用不断变化的世界中,您可能已经注意到动画的增加,例如脉冲按钮、过渡、移动背景等等。当 GUI 应用使用动画时,它们可以向用户提供视觉提示,让他们知道下一步该做什么。有了 JavaFX,您可以两全其美。

图 15-1 展示了一幅栩栩如生的简单图画。

A323910_3_En_15_Fig1_HTML.jpg

图 15-1。JavaFX 图形

在这一章中,你将创建图像,动画,外观和感觉。系好你的安全带;您将发现将酷炫的游戏式界面融入日常应用的解决方案。

注意

如果您不熟悉 JavaFX,请参考第十四章。其中,它将帮助您创建一个使用 JavaFX 提高工作效率的环境。

15-1.创建图像

问题

您的文件目录中有一些照片,您希望快速浏览并在图形用户界面中展示。

解决办法

创建一个简单的 JavaFX 图像查看器应用。这个配方中使用的主要 Java 类是:

  • Java VX . scene . image . image

  • javafx.scene.image.ImageView

  • 事件处理程序

以下源代码是图像查看器应用的实现:

package org.java9recipes.chapter15.recipe15_01;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

/**
 * Recipe 15-1: Creating Images
 *
 * @author cdea
 * Update: J Juneau
 */
public class CreatingImages extends Application {

    private final List<String> imageFiles = new ArrayList<>();
    private int currentIndex = -1;
    private final String filePrefix = "file:";

    public enum ButtonMove {

        NEXT, PREV
    };

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Chapter 15-1 Creating a Image");
        Group root = new Group();
        Scene scene = new Scene(root, 551, 400, Color.BLACK);

        // image view
        final ImageView currentImageView = new ImageView();

        // maintain aspect ratio
        currentImageView.setPreserveRatio(true);

        // resize based on the scene
        currentImageView.fitWidthProperty().bind(scene.widthProperty());

        final HBox pictureRegion = new HBox();
        pictureRegion.getChildren().add(currentImageView);
        root.getChildren().add(pictureRegion);

        // Dragging over surface
        scene.setOnDragOver((DragEvent event) -> {
            Dragboard db = event.getDragboard();
            if (db.hasFiles()) {
                event.acceptTransferModes(TransferMode.COPY);
            } else {
                event.consume();
            }
        });

        // Dropping over surface
        scene.setOnDragDropped((DragEvent event) -> {
            Dragboard db = event.getDragboard();
            boolean success = false;
            if (db.hasFiles()) {
                success = true;
                String filePath = null;
                for (File file : db.getFiles()) {
                    filePath = file.getAbsolutePath();
                    System.out.println(filePath);
                    currentIndex += 1;
                    imageFiles.add(currentIndex, filePath);
                }
                filePath = filePrefix + filePath;
                // set new image as the image to show.
                Image imageimage = new Image(filePath);
                currentImageView.setImage(imageimage);

            }
            event.setDropCompleted(success);
            event.consume();

        });

        // create slide controls
        Group buttonGroup = new Group();

        // rounded rect
        Rectangle buttonArea = new Rectangle();
        buttonArea.setArcWidth(15);
        buttonArea.setArcHeight(20);
        buttonArea.setFill(new Color(0, 0, 0, .55));
        buttonArea.setX(0);
        buttonArea.setY(0);
        buttonArea.setWidth(60);
        buttonArea.setHeight(30);
        buttonArea.setStroke(Color.rgb(255, 255, 255, .70));

        buttonGroup.getChildren().add(buttonArea);
        // left control
        Arc leftButton = new Arc();
        leftButton.setType(ArcType.ROUND);
        leftButton.setCenterX(12);
        leftButton.setCenterY(16);
        leftButton.setRadiusX(15);
        leftButton.setRadiusY(15);
        leftButton.setStartAngle(-30);
        leftButton.setLength(60);
        leftButton.setFill(new Color(1, 1, 1, .90));

        leftButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
            int indx = gotoImageIndex(ButtonMove.PREV);
            if (indx > -1) {
                String namePict = imageFiles.get(indx);
                namePict = filePrefix + namePict;
                final Image image = new Image(namePict);
                currentImageView.setImage(image);
            }
        });
        buttonGroup.getChildren().add(leftButton);

        // right control
        Arc rightButton = new Arc();
        rightButton.setType(ArcType.ROUND);
        rightButton.setCenterX(12);
        rightButton.setCenterY(16);
        rightButton.setRadiusX(15);
        rightButton.setRadiusY(15);
        rightButton.setStartAngle(180 - 30);
        rightButton.setLength(60);
        rightButton.setFill(new Color(1, 1, 1, .90));
        rightButton.setTranslateX(40);
        buttonGroup.getChildren().add(rightButton);

        rightButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
            int indx = gotoImageIndex(ButtonMove.NEXT);
            if (indx > -1) {
                String namePict = imageFiles.get(indx);
                namePict = filePrefix + namePict;
                final Image image = new Image(namePict);
                currentImageView.setImage(image);
            }
        });

        // move button group when scene is resized
        buttonGroup.translateXProperty().bind(scene.widthProperty().subtract(buttonArea.getWidth() + 6));
        buttonGroup.translateYProperty().bind(scene.heightProperty().subtract(buttonArea.getHeight() + 6));
        root.getChildren().add(buttonGroup);

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * Returns the next index in the list of files to go to next.
     *
     * @param direction PREV and NEXT to move backward or forward in the list of
     * pictures.
     * @return int the index to the previous or next picture to be shown.
     */
    public int gotoImageIndex(ButtonMove direction) {
        int size = imageFiles.size();
        if (size == 0) {
            currentIndex = -1;
        } else if (direction == ButtonMove.NEXT && size > 1 && currentIndex < size - 1) {
            currentIndex += 1;
        } else if (direction == ButtonMove.PREV && size > 1 && currentIndex > 0) {
            currentIndex -= 1;
        }

        return currentIndex;
    }

图 15-2 描述了拖放操作,该操作在表面上用缩略图大小的图像给用户视觉反馈。在图中,我将图像拖到应用窗口上。

A323910_3_En_15_Fig2_HTML.jpg

图 15-2。拖放正在进行中

图 15-3 显示放下操作已成功加载图像。

A323910_3_En_15_Fig3_HTML.jpg

图 15-3。删除操作完成

它是如何工作的

这是一个简单的应用,允许您查看具有如下文件格式的图像。jpg,。加载图像需要使用鼠标将文件拖放到窗口区域。该应用还允许您调整窗口大小,这将自动导致图像缩放,同时保持其纵横比。几幅图像加载成功后,通过点击左右按钮控件,可以方便地翻阅每幅图像,如图 15-3 所示。

在遍历代码之前,让我们讨论一下应用的变量。表 15-1 描述了这个圆滑的图像浏览器应用的实例变量。

表 15-1。CreatingImages 实例变量
|

可变的

|

数据类型

|

例子

|

描述

|
| --- | --- | --- | --- |
| 映像文件 | 列表 | /User/pictures/fun.jpg | 字符串列表,每个字符串包含图像的绝对文件路径 |
| 当前值的索引 | (同 Internationalorganizations)国际组织 | Zero | imageFiles 列表中的零相对索引号;-1 表示没有图像可查看 |
| 然后 | 列举型别 | - | 用户单击右箭头按钮 |
| 上一个 | 列举型别 | - | 用户单击左箭头按钮 |

当您将图像拖动到应用中时,image file 变量会将绝对文件路径缓存为字符串,而不是实际的图像文件,以节省内存空间。如果用户将同一个图像文件拖动到显示区域,列表将包含表示该图像文件的重复字符串。显示图像时,currentIndex 变量包含 imageFiles 列表的索引。imageFiles 列表指向表示当前图像文件的字符串。当用户点击按钮显示上一幅和下一幅图像时,currentIndex 将分别递减或递增。接下来,让我们浏览代码,详细说明加载和显示图像的步骤。稍后,您将学习使用“下一页”和“上一页”按钮翻阅每幅图像的步骤。

首先实例化 javafx.scene.image.ImageView 类的一个实例。ImageView 类是一个图形节点(node ),用于显示已经加载的 javafx.scene.image.Image 对象。使用 ImageView 节点将使您能够在不操作物理图像的情况下在要显示的图像上创建特殊效果。为了避免在呈现许多效果时性能下降,可以使用引用单个 Image 对象的多个 ImageView 对象。许多类型的效果包括模糊、淡化和变换图像。

需求之一是在用户调整窗口大小时保持显示图像的纵横比。这里,您只需调用值为 true 的 setPreserveRatio()方法来保持图像的纵横比。请记住,因为用户调整了窗口的大小,所以您希望将 ImageView 的宽度绑定到场景的宽度,以允许缩放图像。设置 ImageView 后,您会希望将它传递给一个 HBox 实例(pictureRegion)以放入场景中。以下代码创建 ImageView 实例,保留纵横比,并缩放图像:

// image view
final ImageView currentImageView = new ImageView();

// maintain aspect ratio
currentImageView.setPreserveRatio(true);

// resize based on the scene
currentImageView.fitWidthProperty().bind(scene.widthProperty());

接下来,让我们介绍 JavaFX 的原生拖放支持,它为用户提供了许多选项,例如将可视对象从一个应用拖放到另一个应用中。在这种情况下,用户将把图像文件从主机窗口操作系统拖到图像查看器应用。在这种情况下,必须生成 EventHandler 对象来侦听 DragEvents。为了满足这一要求,您将设置场景的拖放事件处理程序方法。

要设置拖动属性,请使用适当的通用 EventHandler 类型调用场景的 setOnDragOver()方法。在示例中,lambda 表达式用于实现事件处理程序。通过 lambda 表达式实现 handle()方法来监听拖动事件(DragEvent)。在事件处理程序中,请注意事件(DragEvent)对象对 getDragboard()方法的调用。对 getDragboard()的调用将返回拖动源(Dragboard),也就是广为人知的剪贴板。一旦获得 Dragboard 对象,就可以确定和验证在表面上拖动的是什么。在这种情况下,您需要确定 Dragboard 对象是否包含任何文件。如果是,则通过传入常量 TransferMode 来调用事件对象的 acceptTransferModes()。向应用的用户提供视觉反馈(参见图 15-2 )。否则,它应该通过调用 event.consume()方法来使用事件。以下代码演示了如何设置场景的 OnDragOver 属性:

    // Dragging over surface
scene.setOnDragOver((DragEvent event) -> {
    Dragboard db = event.getDragboard();
    if (db.hasFiles()) {
        event.acceptTransferModes(TransferMode.COPY);
    } else {
        event.consume();
    }
});

一旦设置了拖放事件处理程序属性,就可以创建一个拖放事件处理程序属性,这样它就可以完成操作。监听拖放事件类似于监听拖动事件,其中 handle()方法将通过 lambda 表达式实现。您再次从事件中获取 Dragboard 对象,以确定剪贴板中是否包含任何文件。如果是,则迭代文件列表,并将文件名添加到 imageFiles 列表中。此代码演示了如何设置场景的 OnDragDropped 属性:

// Dropping over surface
scene.setOnDragDropped((DragEvent event) -> {
    Dragboard db = event.getDragboard();
    boolean success = false;
    if (db.hasFiles()) {
        success = true;
        String filePath = null;
        for (File file : db.getFiles()) {
            filePath = file.getAbsolutePath();
            System.out.println(filePath);
            currentIndex += 1;
            imageFiles.add(currentIndex, filePath);
        }
        filePath = filePrefix + filePath;
        // set new image as the image to show.
        Image imageimage = new Image(filePath);
        currentImageView.setImage(imageimage);

    }
    event.setDropCompleted(success);
    event.consume();

});

确定最后一个文件后,将显示当前图像。下面的代码演示了如何加载要显示的图像:

// set new image as the image to show.
Image imageimage = new Image(filePath);
currentImageView.setImage(imageimage);

对于与图像查看器应用相关的最后一个需求,生成了允许用户查看下一个或上一个图像的简单控件。我强调“简单”控件是因为 JavaFX 包含另外两种创建自定义控件的方法。一种方法,层叠样式表(CSS)样式,将在后面的食谱 15-5 中讨论。要探索另一种选择,请参考皮肤上的 Javadoc 和可设置皮肤的 API。

本例中的简单按钮是使用 Java FX 的 javafx.scene.shape.Arc 在一个名为 javafx.scene.shape.Rectangle 的小透明圆角矩形上构建左右箭头而创建的。上一页和按钮移动下一页

当实例化一个在< and >符号之间有类型变量的泛型类时,相同的类型变量将在 handle()的签名中定义。当实现事件处理程序逻辑时,您确定按下了哪个按钮,然后将索引返回到要显示的下一个图像的 imageFiles 列表中。当使用 image 类加载图像时,可以从文件系统或 URL 加载图像。以下代码实例化一个 EventHandler lambda 表达式,以显示 imageFiles 列表中的前一幅图像:

leftButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
    int indx = gotoImageIndex(ButtonMove.PREV);
    if (indx > -1) {
        String namePict = imageFiles.get(indx);
        namePict = filePrefix + namePict;
        final Image image = new Image(namePict);
        currentImageView.setImage(image);
    }
});

右按钮的(right button)事件处理程序是相同的。唯一不同的是,它必须确定是否通过 ButtonMove 枚举按下了上一个或下一个按钮。该信息被传递给 gotoImageIndex()方法,以确定图像在该方向上是否可用。

为了完成 image viewer 应用,您将矩形按钮的控件绑定到场景的宽度和高度,这将在用户调整窗口大小时重新定位控件。这里,通过减去 buttonArea 的宽度(Fluent API),将 translateXProperty()绑定到场景的 width 属性。在该示例中,还根据场景的 height 属性绑定了 translateYProperty()。一旦你的按钮控件被绑定,你的用户将体验到良好的用户界面。以下代码使用 Fluent API 将按钮控件的属性绑定到场景的属性:

// move button group when scene is resized        buttonGroup.translateXProperty().bind(scene.widthProperty().subtract(buttonArea.getWidth()
   + 6));
buttonGroup.translateYProperty().bind(scene.heightProperty().subtract(buttonArea.getHeight()
   + 6));
root.getChildren().add(buttonGroup);

15-2.生成动画

问题

你想生成一个动画。例如,您希望创建一个新闻收报机和照片查看器应用,并满足以下要求:

  • 它将有一个向左滚动的新闻滚动条控件。

  • 当用户点击按钮控件时,它将淡出当前图片并淡入下一张图片。

  • 当光标移入和移出场景区域时,它将分别淡入和淡出按钮控件。

  • 当鼠标悬停在文本上时,新闻滚动条会暂停,当鼠标离开文本时,它会重新开始。

解决办法

通过访问 JavaFX 的动画 API(Java FX . animation . *)创建动画效果。要创建前面提到的新闻收报机,您需要以下类:

  • Java FX . animation . translate transition

  • javafx.util.Duration

  • javafx.event.EventHandler

  • javafx.scene.shape.Rectangle

要淡出当前图片并淡入下一张图片,您需要以下类:

  • Java FX . animation . sequential transition

  • Java FX . animation . fade transition

  • javafx.event.EventHandler

  • Java VX . scene . image . image

  • javafx.scene.image.ImageView

  • javafx.util.Duration

要在光标移入和移出场景区域时分别淡入和淡出按钮控件,您需要以下类:

  • Java FX . animation . fade transition

  • javafx.util.Duration

此处显示的是用于创建新闻滚动条控件的代码:

// create ticker area
final Group tickerArea = new Group();
final Rectangle tickerRect = new Rectangle();
tickerRect.setArcWidth(15);
tickerRect.setArcHeight(20);
tickerRect.setFill(new Color(0, 0, 0, .55));
tickerRect.setX(0);
tickerRect.setY(0);
tickerRect.setWidth(scene.getWidth() - 6);
tickerRect.setHeight(30);
tickerRect.setStroke(Color.rgb(255, 255, 255, .70));

Rectangle clipRegion = new Rectangle();
clipRegion.setArcWidth(15);
clipRegion.setArcHeight(20);
clipRegion.setX(0);
clipRegion.setY(0);
clipRegion.setWidth(scene.getWidth() - 6);
clipRegion.setHeight(30);
clipRegion.setStroke(Color.rgb(255, 255, 255, .70));

tickerArea.setClip(clipRegion);

// Resize the ticker area when the window is resized
tickerArea.setTranslateX(6);
tickerArea.translateYProperty().bind(scene.heightProperty().subtract(
    tickerRect.getHeight() + 6));
tickerRect.widthProperty().bind(scene.widthProperty().subtract(
    buttonRect.getWidth() + 16));
clipRegion.widthProperty().bind(scene.widthProperty().subtract(
    buttonRect.getWidth() + 16));
tickerArea.getChildren().add(tickerRect);

root.getChildren().add(tickerArea);

// add news text
Text news = new Text();
news.setText("JavaFX 8 News Ticker... | New Features: Swing Node, Event Dispatch Thread and JavaFX Application Thread Merge,  " +
        "New Look and Feel - Modena, Rich Text Support, Printing, Tree Table Control, Much More!");
news.setTranslateY(18);
news.setFill(Color.WHITE);
tickerArea.getChildren().add(news);

final TranslateTransition ticker = new TranslateTransition();
ticker.setNode(news);
int newsLength = news.getText().length();

// Calculated guess based upon length of text
ticker.setDuration(Duration.millis((newsLength * 4/300) * 15000));
ticker.setFromX(scene.widthProperty().doubleValue());
ticker.setToX(-scene.widthProperty().doubleValue() - (newsLength * 5));
ticker.setFromY(19);
ticker.setInterpolator(Interpolator.LINEAR);
ticker.setCycleCount(1);

// when ticker has finished reset and replay ticker animation
ticker.setOnFinished((ActionEvent ae) -> {
    ticker.stop();
    ticker.setFromX(scene.getWidth());
    ticker.setDuration(new Duration((newsLength * 4/300) * 15000));
    ticker.playFromStart();
});

// stop ticker if hovered over
tickerArea.setOnMouseEntered((MouseEvent me) -> {
    ticker.pause();
});

// restart ticker if mouse leaves the ticker
tickerArea.setOnMouseExited((MouseEvent me) -> {
    ticker.play();
});

ticker.play();

下面是用于淡出当前图片和淡入下一张图片的代码:

    // previous button
    Arc prevButton = // create arc ...

    prevButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
        int indx = gotoImageIndex(PREV);
        if (indx > -1) {
            String namePict = imagesFiles.get(indx);
            final Image nextImage = new Image(namePict);
            SequentialTransition seqTransition = transitionByFading(nextImage, currentImageView);
            seqTransition.play();
        }
    });

    buttonGroup.getChildren().add(prevButton);

    // next button
    Arc nextButton = //... create arc

    buttonGroup.getChildren().add(nextButton);

    nextButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
        int indx = gotoImageIndex(NEXT);
        if (indx > -1) {
            String namePict = imagesFiles.get(indx);
            final Image nextImage = new Image(namePict);
            SequentialTransition seqTransition = transitionByFading(nextImage, currentImageView);
            seqTransition.play();

        }
    });

//... the rest of the start(Stage primaryStage) method

public int gotoImageIndex(int direction) {
    int size = imagesFiles.size();
    if (size == 0) {
        currentIndexImageFile = -1;
    } else if (direction == NEXT && size > 1 && currentIndexImageFile < size - 1) {
        currentIndexImageFile += 1;
    } else if (direction == PREV && size > 1 && currentIndexImageFile > 0) {
        currentIndexImageFile -= 1;
    }

    return currentIndexImageFile;
}

public SequentialTransition transitionByFading(final Image nextImage, final ImageView imageView) {
    FadeTransition fadeOut = new FadeTransition(Duration.millis(500), imageView);
    fadeOut.setFromValue(1.0);
    fadeOut.setToValue(0.0);
    fadeOut.setOnFinished((ActionEvent ae) -> {
        imageView.setImage(nextImage);
    });
    FadeTransition fadeIn = new FadeTransition(Duration.millis(500), imageView);
    fadeIn.setFromValue(0.0);
    fadeIn.setToValue(1.0);
    SequentialTransition seqTransition = new SequentialTransition();
    seqTransition.getChildren().addAll(fadeOut, fadeIn);
    return seqTransition;
}

以下代码用于在光标移入和移出场景区域时分别淡入和淡出按钮控件:

// Fade in button controls
scene.setOnMouseEntered((MouseEvent me) -> {
    FadeTransition fadeButtons = new FadeTransition(Duration.millis(500), buttonGroup);
    fadeButtons.setFromValue(0.0);
    fadeButtons.setToValue(1.0);
    fadeButtons.play();
});

// Fade out button controls
scene.setOnMouseExited((MouseEvent me) -> {
    FadeTransition fadeButtons = new FadeTransition(Duration.millis(500), buttonGroup);
    fadeButtons.setFromValue(1);
    fadeButtons.setToValue(0);
    fadeButtons.play();
});

图 15-4 显示了在屏幕底部区域带有滚动条控件的照片查看器应用。

A323910_3_En_15_Fig4_HTML.jpg

图 15-4。带有新闻栏的照片查看器

它是如何工作的

这个食谱采用了食谱 15-1 中的照片浏览器应用,并添加了一个新闻栏和一些漂亮的照片变换动画。主要动画效果集中在平移和淡入淡出。首先,创建一个 news ticker 控件,它通过使用 translation transition(Java FX . animation . translate transition)向左滚动文本节点。接下来,应用另一个淡入淡出效果,以便当用户单击“上一个”和“下一个”按钮过渡到下一个图像时,会出现缓慢的过渡。要实现这种效果,需要使用复合过渡(Java FX . animation . sequential transition ),由多个动画组成。最后,要创建按钮控件根据鼠标位置淡入淡出的效果,需要使用一个渐变过渡(Java FX . animation . fade transition)。

在我开始讨论满足需求的步骤之前,我想提一下 JavaFX 动画的基础知识。JavaFX animation API 允许您组装定时事件,这些事件可以在节点的属性值上进行插值以产生动画效果。每个定时事件称为一个关键帧(keyframe),它负责在一段时间内对节点的属性进行插值(javafx.util.Duration)。知道关键帧的工作是对节点的属性值进行操作,您必须创建一个引用所需节点属性的 KeyValue 类的实例。插值的概念就是在起始值和结束值之间分配值。一个例子是在 1,000 毫秒内将矩形的当前 x 位置(零)移动到 100 个像素;换句话说,在一秒钟内将矩形向右移动 100 个像素。此处显示的是一个关键帧和关键值,用于对矩形的 x 属性进行 1000 毫秒的插值:

final Rectangle rectangle = new Rectangle(0, 0, 50, 50);
KeyValue keyValue = new KeyValue(rectangle.xProperty(), 100);
KeyFrame keyFrame = new KeyFrame(Duration.millis(1000), keyValue);

当创建许多连续组合的关键帧时,您需要创建一个时间轴。因为 timeline 是 javafx.animation.Animation 的子类,所以您可以设置一些标准属性,例如它的循环计数和自动反转。循环计数是您希望时间轴播放动画的次数。如果希望循环计数无限期播放动画,请使用值 Timeline.INDEFINITE。自动反转是动画向后播放时间线的功能。默认情况下,周期计数设置为 1,自动冲销设置为 false。添加关键帧时,只需使用 getKeyFrames()添加即可。时间轴对象上的 add()方法。以下代码片段演示了 autoreverse 设置为 true 时无限播放的时间轴:

Timeline timeline = new Timeline();
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.setAutoReverse(true);
timeline.getKeyFrames().add(keyFrame);
timeline.play();

有了时间线的知识,您可以在 JavaFX 中制作任何图形节点的动画。虽然你可以用一种简单的方式来创建时间线,但这会变得非常麻烦。你可能想知道是否有更简单的方法来表达常见的动画。好消息!JavaFX 有转换(Transition),这是执行常见动画效果的便利类。您可以使用转场创建的一些常见动画效果包括:

  • Java FX . animation . fade transition

  • Java FX . animation . path transition

  • Java FX . animation . scale transition

  • Java FX . animation . translate transition

要查看更多过渡,请参见 Javadoc 中的 javafx.animation。因为过渡对象也是 javafx.animation.Animation 类的子类,所以可以设置循环计数和自动反转属性。这个菜谱着重于两种过渡效果:平移过渡(translate transition)和淡化过渡(fade transition)。

问题陈述中的第一个要求是创建一个新闻收报机。在新闻滚动条控件中,文本节点在矩形区域内从右向左滚动。当文本滚动到矩形区域的左边缘时,您会希望文本被剪切以创建一个仅显示矩形内部像素的视口。为此,首先创建一个组来保存组成 ticker 控件的所有组件。接下来,你创建一个白色圆角矩形填充 55%的不透明度。创建可视区域后,使用 Group 对象上的 setClip(someRectangle)方法创建一个表示剪辑区域的类似矩形。图 15-5 显示了一个圆角矩形区域,作为剪切区域。

A323910_3_En_15_Fig5_HTML.jpg

图 15-5。在组对象上设置剪辑区域

一旦创建了 ticker 控件,就可以根据场景的 height 属性减去 ticker 控件的高度来绑定 translate Y。还可以根据场景的宽度减去按钮控件的宽度来绑定 ticker 控件的 width 属性。通过绑定这些属性,每当用户调整应用窗口大小时,滚动条控件可以更改其大小和位置。这使得滚动条控件看起来浮动在窗口的底部。下面的代码绑定了 ticker 控件的 translate Y、width 和剪辑区域的 width 属性:

tickerArea.translateYProperty().bind(scene.heightProperty().subtract(tickerRect.getHeight() + 6));
tickerRect.widthProperty().bind(scene.widthProperty().subtract(buttonRect.getWidth() + 16));
clipRegion.widthProperty().bind(scene.widthProperty().subtract(buttonRect.getWidth() + 16));
tickerArea.getChildren().add(tickerRect);

现在 ticker 控件已经完成,您将创建一些新闻来填充它。在本例中,使用了一个文本节点,该节点包含表示新闻提要的文本。要向 ticker 控件添加新创建的文本节点,可以调用它的 getChildren()。add()方法。下面的代码向 ticker 控件添加一个文本节点:

final Group tickerArea = new Group();
final Rectangle tickerRect = //...
Text news = new Text();
news.setText("JavaFX 8 News Ticker... | New Features: Swing Node, Event Dispatch Thread and JavaFX Application Thread Merge,  " +
        "New Look and Feel - Modena, Rich Text Support, Printing, Tree Table Control, Much More!");
news.setTranslateY(18);
news.setFill(Color.WHITE);
tickerArea.getChildren().add(news);

接下来,您必须使用 JavaFX 的 TranslateTransition API 从右向左滚动文本节点。第一步是设置目标节点来执行 TranslateTransition。然后设置持续时间,这是 TranslateTransition 制作动画所花费的总时间。TranslateTransition 通过公开对节点的 translate X 和 Y 属性进行操作的便利方法,简化了动画的创建。方便的方法前面加上 from 和 to。例如,在文本节点上使用 translate X 的场景中,有 fromX()和 toX()方法。fromX()是开始值,toX()是将被插值的结束值。在示例中,这些计算基于文本节点中的文本长度。因此,如果您从一个远程源(比如一个 RSS 提要)阅读,文本长度的差异应该不会影响滚动条。接下来,将 TranslateTransition 设置为线性过渡(插值器。线性)在起始值和结束值之间均匀插值。要查看更多插值器类型或查看如何创建自定义插值器,请参阅 Java FX . animation . interpolator 上的 Javadoc。最后,在示例中,循环计数设置为 1,这将根据指定的持续时间动画显示一次跑马灯。下面的代码片段详细说明了如何创建 TranslateTransition,该 Transition 从右向左动画显示文本节点:

final TranslateTransition ticker = new TranslateTransition();
ticker.setNode(news);
int newsLength = news.getText().length();
ticker.setDuration(Duration.millis((newsLength * 4/300) * 15000));
ticker.setFromX(scene.widthProperty().doubleValue());
ticker.setToX(-scene.widthProperty().doubleValue() - (newsLength * 5));
ticker.setFromY(19);
ticker.setInterpolator(Interpolator.LINEAR);
ticker.setCycleCount(1);

当滚动条的新闻完全滚出滚动条区域到场景的最左边时,您会想要停止并从头(最右边)重放新闻提要。为此,通过 lambda 表达式创建 EventHandler 对象的实例,并使用 setOnFinished()方法在 ticker (TranslateTransition)对象上设置该实例。以下是如何重放平移动画:

// when window resizes width wise the ticker will know how far to move
// when ticker has finished reset and replay ticker animation
ticker.setOnFinished((ActionEvent ae) -> {
    ticker.stop();
    ticker.setFromX(scene.getWidth());
    ticker.setDuration(new Duration((newsLength * 4/300) * 15000));
    ticker.playFromStart();
});

一旦定义了动画,只需调用 play()方法就可以开始播放。以下代码片段显示了如何播放 TranslateTransition:

ticker.play();

要在鼠标悬停并离开文本时暂停和启动滚动条,需要实现类似的事件处理程序:

// stop ticker if hovered over
tickerArea.setOnMouseEntered((MouseEvent me) -> {
    ticker.pause();
});

// restart ticker if mouse leaves the ticker
tickerArea.setOnMouseExited((MouseEvent me) -> {
    ticker.play();
});

现在您对动画转场有了更好的理解,那么可以触发任意数量转场的转场呢?JavaFX 有两个提供这种行为的转换。这两个转换可以顺序或并行调用独立的从属转换。在这个菜谱中,您将使用一个顺序过渡(sequential transition)来包含两个 FadeTransitions,以便淡出当前显示的图像并淡入下一个图像。创建 previous 和 next 按钮的事件处理程序时,首先通过调用 gotoImageIndex()方法确定要显示的下一个图像。一旦确定了要显示的下一个图像,就调用 transitionByFading()方法,该方法返回 SequentialTransition 的一个实例。当调用 transitionByFading()方法时,您会注意到创建了两个 FadeTransitions。第一个过渡将不透明度级别从 1.0 更改为 0.0,以淡出当前图像,第二个过渡将不透明度级别从 0.0 插值到 1.0,淡入下一个图像,该图像随后成为当前图像。最后,这两个 FadeTransitions 被添加到 SequentialTransition 中并返回给调用者。以下代码创建两个 FadeTransitions 并将它们添加到 SequentialTransition 中:

FadeTransition fadeOut = new FadeTransition(Duration.millis(500), imageView);
fadeOut.setFromValue(1.0);
fadeOut.setToValue(0.0);
fadeOut.setOnFinished((ActionEvent ae) -> {
    imageView.setImage(nextImage);
});
FadeTransition fadeIn = new FadeTransition(Duration.millis(500), imageView);
fadeIn.setFromValue(0.0);
fadeIn.setToValue(1.0);
SequentialTransition seqTransition = new SequentialTransition();
seqTransition.getChildren().addAll(fadeOut, fadeIn);
return seqTransition;

对于与渐强和渐弱相关的最后要求,请使用按钮控制。使用 FadeTransition 创建幽灵般的动画效果。首先,创建一个 EventHandler(更具体地说,通过 lambda 表达式创建一个 EventHandler )。很容易将鼠标事件添加到场景中;您所要做的就是覆盖 handle()方法,其中入站参数是 MouseEvent 类型(与其形式类型参数相同)。在 lambda 内部,通过使用将持续时间和节点作为参数的构造函数来创建 FadeTransition 对象的实例。接下来,您会注意到调用 setFromValue()和 setToValue()方法来为不透明度插入 1.0 和 0.0 之间的值,从而产生淡入效果。下面的代码添加了一个事件处理程序,用于在鼠标光标位于场景内部时创建淡入效果:

// Fade in button controls
scene.setOnMouseEntered((MouseEvent me) -> {
    FadeTransition fadeButtons = new FadeTransition(Duration.millis(500), buttonGroup);
    fadeButtons.setFromValue(0.0);
    fadeButtons.setToValue(1.0);
    fadeButtons.play();
});

最后但同样重要的是,淡出事件处理程序基本上与淡入相同,除了不透明度 From 和 To 值从 1.0 到 0.0,这使得当鼠标指针离开场景区域时,按钮神秘地消失。

15-3.沿路径制作形状动画

问题

您想要创建一种方式来沿路径动画形状。

解决办法

创建一个应用,允许用户绘制一个形状的路径。这个配方中使用的主要 Java 类是:

  • Java FX . animation . path transition

  • javafx.scene.input.MouseEvent

  • javafx.event.EventHandler

  • javafx.geometry.Point2D

  • javafx.scene.shape.LineTo

  • javafx.scene.shape.MoveTo

  • javafx.scene.shape.Path

下面的代码演示了如何绘制形状的路径:

package org.java9recipes.chapter15.recipe15_03;

import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
import javafx.util.Duration;

/**
 * Recipe 15-3: Working with the Scene Graph
 * @author cdea
 * Update: J Juneau
 */
public class WorkingWithTheSceneGraph extends Application {

    Path onePath = new Path();
    Point2D anchorPt;
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Chapter 15-3 Working with the Scene Graph");

        final Group root = new Group();
        // add path
        root.getChildren().add(onePath);

        final Scene scene = new Scene(root, 300, 250);
        scene.setFill(Color.WHITE);

        RadialGradient gradient1 = new RadialGradient(0,
                .1,
                100,
                100,
                20,
                false,
                CycleMethod.NO_CYCLE,
                new Stop(0, Color.RED),
                new Stop(1, Color.BLACK));

        // create a sphere
        final Circle sphere = new Circle();
        sphere.setCenterX(100);
        sphere.setCenterY(100);
        sphere.setRadius(20);
        sphere.setFill(gradient1);

        // add sphere
        root.getChildren().add(sphere);

        // animate sphere by following the path.
        final PathTransition pathTransition = new PathTransition();
        pathTransition.setDuration(Duration.millis(4000));
        pathTransition.setCycleCount(1);
        pathTransition.setNode(sphere);
        pathTransition.setPath(onePath);
        pathTransition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);

        // once finished clear path
        pathTransition.onFinishedProperty().set((EventHandler<ActionEvent>)
        (ActionEvent event) -> {
            onePath.getElements().clear();
        });

        // starting initial path
        scene.onMousePressedProperty().set((EventHandler<MouseEvent>)
        (MouseEvent event) -> {
            onePath.getElements().clear();
            // start point in path
            anchorPt = new Point2D(event.getX(), event.getY());
            onePath.setStrokeWidth(3);
            onePath.setStroke(Color.BLACK);
            onePath.getElements().add(new MoveTo(anchorPt.getX(), anchorPt.getY()));
        });

        // dragging creates lineTos added to the path
        scene.onMouseDraggedProperty().set((EventHandler<MouseEvent>)
        (MouseEvent event) -> {
            onePath.getElements().add(new LineTo(event.getX(), event.getY()));
        });

        // end the path when mouse released event
        scene.onMouseReleasedProperty().set((EventHandler<MouseEvent>)
        (MouseEvent event) -> {
            onePath.setStrokeWidth(0);
            if (onePath.getElements().size() > 1) {
                pathTransition.stop();
                pathTransition.playFromStart();
            }
        });

        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

图 15-6 显示了圆将遵循的绘制路径。当用户执行鼠标释放时,绘制的路径将消失,红球将遵循之前绘制的路径。

A323910_3_En_15_Fig6_HTML.jpg

图 15-6。路径转换

它是如何工作的

在这个菜谱中,您将创建一个简单的应用,使对象能够沿着场景图上绘制的路径移动。为了简单起见,该示例使用了一个执行路径转换(Java FX . animation . path transition)的形状(圆形)。应用用户将像绘图程序一样通过按下鼠标按钮在场景表面上绘制路径。一旦对绘制的路径感到满意,用户释放鼠标按键,这将触发红球沿着路径移动,类似于物体在建筑物内的管道中移动。

首先创建两个实例变量来维护组成路径的坐标。要保存正在绘制的路径,请创建 javafx.scene.shape.Path 对象的实例。在应用启动之前,应该将路径实例添加到场景图中。此处显示的是将实例变量 onePath 添加到场景图的过程:

// add path
root.getChildren().add(onePath);

接下来,创建一个实例变量 anchor pt(Java FX . geometry . point 2d ),它将保存路径的起点。稍后,您将看到这些变量是如何基于鼠标事件更新的。此处显示的是维护当前绘制路径的实例变量:

Path onePath = new Path();
Point2D anchorPt;

首先,让我们创建一个动画形状。在这种情况下,您将创建一个看起来很酷的红色球。要创建一个看起来像球形的球,创建一个渐变颜色 RadialGradient,用于绘制或填充圆形。(参考配方 15-6,了解如何用渐变颜料填充形状。)一旦创建了红色球体,就需要创建 PathTransition 对象来执行路径跟踪动画。实例化 PathTransition()对象后,只需将持续时间设置为 4 秒,并将循环计数设置为 1。循环计数是动画循环发生的次数。接下来,将节点设置为引用红色球(球体)。然后,将 path()方法设置为实例变量 onePath,该变量包含构成绘制路径的所有坐标和线条。为球体设置动画路径后,您应该指定形状如何跟随路径,例如垂直于路径上的切点。下面的代码创建了一个路径转换的实例:

// animate sphere by following the path.
final PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(4000));
pathTransition.setCycleCount(1);
pathTransition.setNode(sphere);
pathTransition.setPath(onePath);
pathTransition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);

创建路径过渡后,您会希望它在动画完成时清理干净。若要在动画结束时重置或清除 path 变量,请创建并添加一个事件处理程序来侦听 path transition 对象上的 onFinished 属性事件。

以下代码片段添加了一个事件处理程序来清除当前路径信息:

// once finished clear path
pathTransition.onFinishedProperty().set((EventHandler<ActionEvent>)
(ActionEvent event) -> {
            onePath.getElements().clear();
});

形状和转换都设置好了,应用需要响应鼠标事件,这将更新前面提到的实例变量。为此,请侦听场景对象上发生的鼠标事件。这里,您将再次依赖于创建事件处理程序来设置场景的 onMouseXXXProperty 方法,其中 XXX 表示实际的鼠标事件名称,如按下、拖动和释放。

当用户绘制路径时,他或她将执行鼠标按下事件来开始路径的起点。若要侦听鼠标按下事件,请使用 MouseEvent 的正式类型参数创建一个事件处理程序。在示例中,使用了 lambda 表达式。当鼠标按下事件发生时,清除任何先前绘制的路径信息的实例变量 onePath。接下来,只需设置路径的笔画宽度和颜色,这样用户就可以看到正在绘制的路径。最后,使用 MoveTo 对象的实例将起点添加到路径中。这里显示的是当用户执行鼠标按压时响应的处理程序代码:

        // starting initial path
   scene.onMousePressedProperty().set((EventHandler<MouseEvent>)
(MouseEvent event) -> {
        onePath.getElements().clear();
        // start point in path
        anchorPt = new Point2D(event.getX(), event.getY());
        onePath.setStrokeWidth(3);
        onePath.setStroke(Color.BLACK);
        onePath.getElements().add(new MoveTo(anchorPt.getX(), anchorPt.getY()));
    });

一旦鼠标按下事件处理程序就绪,就可以为鼠标拖动事件创建另一个处理程序。同样,查找场景的 onMouseXXXProperty()方法,这些方法对应于您所关心的适当鼠标事件。在这种情况下,将设置 onMouseDraggedProperty()。在 lambda 表达式中,获取鼠标坐标,该坐标将被转换为要添加到路径(path)中的 LineTo 对象。这些 LineTo 对象是路径元素(javafx.scene.shape.PathElement)的实例,如配方 15-5 中所述。以下代码是负责鼠标拖动事件的事件处理程序:

// dragging creates lineTos added to the path
scene.onMouseDraggedProperty().set((EventHandler<MouseEvent>)
(MouseEvent event) -> {
        onePath.getElements().add(new LineTo(event.getX(), event.getY()));
});

最后,创建一个事件处理程序来侦听鼠标释放事件。当用户释放鼠标时,路径的描边被设置为零,看起来好像已经被移除。然后,通过停止路径过渡并从头开始播放来重置路径过渡。以下代码是负责鼠标释放事件的事件处理程序:

// end the path when mouse released event
        scene.onMouseReleasedProperty().set((EventHandler<MouseEvent>)
 (MouseEvent event) -> {
            onePath.setStrokeWidth(0);
            if (onePath.getElements().size() > 1) {
                pathTransition.stop();
                pathTransition.playFromStart();
            }
});

15-4.通过网格操纵布局

问题

您希望使用网格类型的布局创建一个好看的基于表单的用户界面。

解决办法

使用 JavaFX 的 javafx.scene.layout.GridPane 创建一个简单的。该应用将具有以下功能:

  • 它将切换网格布局的网格线的显示,以便进行调试。

  • 它将调整 GridPane 的顶部填充。

  • 它将调整 GridPane 的左填充。

  • 它将调整 GridPane 中单元格之间的水平间距。

  • 它将调整 GridPane 中单元格之间的垂直间距。

  • 它将水平对齐单元格内的控件。

  • 它将垂直对齐单元格内的控件。

以下代码是窗体设计器应用的主要启动点:

public class ManipulatingLayoutViaGrids extends Application {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Chapter 15-4 Manipulating Layout via Grids ");
        Group root = new Group();
        Scene scene = new Scene(root, 640, 480, Color.WHITE);

        // Left and right split pane
        SplitPane splitPane = new SplitPane();
        splitPane.prefWidthProperty().bind(scene.widthProperty());
        splitPane.prefHeightProperty().bind(scene.heightProperty());

        // Form on the right
        GridPane rightGridPane = new MyForm();

        GridPane leftGridPane = new GridPaneControlPanel(rightGridPane);

        VBox leftArea = new VBox(10);
        leftArea.getChildren().add(leftGridPane);
        HBox hbox = new HBox();
        hbox.getChildren().add(splitPane);
        root.getChildren().add(hbox);
        splitPane.getItems().addAll(leftArea, rightGridPane);

        primaryStage.setScene(scene);

        primaryStage.show();
    }

}

当窗体设计器应用启动时,要操作的目标窗体显示在窗口的拆分窗格的右侧。下面的代码是一个简单的类似网格的 form 类,它从 GridPane 扩展而来。它将由表单设计器应用操作:

/**
 * MyForm is a form to be manipulated by the user.
 * @author cdea
 */
public class MyForm extends GridPane{
    public MyForm() {

        setPadding(new Insets(5));
        setHgap(5);
        setVgap(5);

        Label fNameLbl = new Label("First Name");
        TextField fNameFld = new TextField();
        Label lNameLbl = new Label("Last Name");
        TextField lNameFld = new TextField();
        Label ageLbl = new Label("Age");
        TextField ageFld = new TextField();

        Button saveButt = new Button("Save");

        // First name label
        GridPane.setHalignment(fNameLbl, HPos.RIGHT);
        add(fNameLbl, 0, 0);

        // Last name label
        GridPane.setHalignment(lNameLbl, HPos.RIGHT);
        add(lNameLbl, 0, 1);

        // Age label
        GridPane.setHalignment(ageLbl, HPos.RIGHT);
        add(ageLbl, 0, 2);

        // First name field
        GridPane.setHalignment(fNameFld, HPos.LEFT);
        add(fNameFld, 1, 0);

        // Last name field
        GridPane.setHalignment(lNameFld, HPos.LEFT);
        add(lNameFld, 1, 1);

        // Age Field
        GridPane.setHalignment(ageFld, HPos.RIGHT);
        add(ageFld, 1, 2);

        // Save button
        GridPane.setHalignment(saveButt, HPos.RIGHT);
        add(saveButt, 1, 3);

    }
}

当应用启动时,网格属性控制面板显示在窗口的拆分窗格的左侧。属性控制面板允许用户动态操作目标表单的网格窗格属性。以下代码表示将操作目标网格窗格属性的网格属性控制面板:

/**
 * GridPaneControlPanel represents the left area of the split pane
 * allowing the user to manipulate the GridPane on the right.
 *
 * Manipulating Layout Via Grids
 * @author cdea
 */
public class GridPaneControlPanel extends GridPane{
    public GridPaneControlPanel(final GridPane targetGridPane) {
        super();

        setPadding(new Insets(5));
        setHgap(5);
        setVgap(5);

        // Setting Grid lines
        Label gridLinesLbl = new Label("Grid Lines");
        final ToggleButton gridLinesToggle = new ToggleButton("Off");
        gridLinesToggle.selectedProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newVal) -> {
            targetGridPane.setGridLinesVisible(newVal);
            gridLinesToggle.setText(newVal ? "On" : "Off");
        });

        // toggle grid lines label
        GridPane.setHalignment(gridLinesLbl, HPos.RIGHT);
        add(gridLinesLbl, 0, 0);

        // toggle grid lines
        GridPane.setHalignment(gridLinesToggle, HPos.LEFT);
        add(gridLinesToggle, 1, 0);

        // Setting padding [top]
        Label gridPaddingLbl = new Label("Top Padding");

        final Slider gridPaddingSlider = new Slider();
        gridPaddingSlider.setMin(0);
        gridPaddingSlider.setMax(100);
        gridPaddingSlider.setValue(5);
        gridPaddingSlider.setShowTickLabels(true);
        gridPaddingSlider.setShowTickMarks(true);
        gridPaddingSlider.setMinorTickCount(1);
        gridPaddingSlider.setBlockIncrement(5);

        gridPaddingSlider.valueProperty().addListener((ObservableValue<? extends Number> ov, Number oldVal, Number newVal) -> {
            double top1 = targetGridPane.getInsets().getTop();
            double right1 = targetGridPane.getInsets().getRight();
            double bottom1 = targetGridPane.getInsets().getBottom();
            double left1 = targetGridPane.getInsets().getLeft();
            Insets newInsets = new Insets((double) newVal, right1, bottom1, left1);
            targetGridPane.setPadding(newInsets);
        });

        // padding adjustment label
        GridPane.setHalignment(gridPaddingLbl, HPos.RIGHT);
        add(gridPaddingLbl, 0, 1);

       // padding adjustment slider
       GridPane.setHalignment(gridPaddingSlider, HPos.LEFT);
       add(gridPaddingSlider, 1, 1);

        // Setting padding [top]
        Label gridPaddingLeftLbl = new Label("Left Padding");

        final Slider gridPaddingLeftSlider = new Slider();
        gridPaddingLeftSlider.setMin(0);
        gridPaddingLeftSlider.setMax(100);
        gridPaddingLeftSlider.setValue(5);
        gridPaddingLeftSlider.setShowTickLabels(true);
        gridPaddingLeftSlider.setShowTickMarks(true);
        gridPaddingLeftSlider.setMinorTickCount(1);
        gridPaddingLeftSlider.setBlockIncrement(5);

        gridPaddingLeftSlider.valueProperty().addListener((ObservableValue<? extends Number> ov, Number oldVal, Number newVal) -> {
            double top1 = targetGridPane.getInsets().getTop();
            double right1 = targetGridPane.getInsets().getRight();
            double bottom1 = targetGridPane.getInsets().getBottom();
            double left1 = targetGridPane.getInsets().getLeft();
            Insets newInsets = new Insets(top1, right1, bottom1, (double) newVal);
            targetGridPane.setPadding(newInsets);
        });

        // padding adjustment label
        GridPane.setHalignment(gridPaddingLeftLbl, HPos.RIGHT);
        add(gridPaddingLeftLbl, 0, 2);

        // padding adjustment slider
        GridPane.setHalignment(gridPaddingLeftSlider, HPos.LEFT);
        add(gridPaddingLeftSlider, 1, 2);

        // Horizontal gap
        Label gridHGapLbl = new Label("Horizontal Gap");

        final Slider gridHGapSlider = new Slider();
        gridHGapSlider.setMin(0);
        gridHGapSlider.setMax(100);
        gridHGapSlider.setValue(5);
        gridHGapSlider.setShowTickLabels(true);
        gridHGapSlider.setShowTickMarks(true);
        gridHGapSlider.setMinorTickCount(1);
        gridHGapSlider.setBlockIncrement(5);

        gridHGapSlider.valueProperty().addListener((ObservableValue<? extends Number> ov, Number oldVal, Number newVal) -> {
            targetGridPane.setHgap((double) newVal);
        });

        // hgap label
        GridPane.setHalignment(gridHGapLbl, HPos.RIGHT);
        add(gridHGapLbl, 0, 3);

       // hgap slider
       GridPane.setHalignment(gridHGapSlider, HPos.LEFT);
       add(gridHGapSlider, 1, 3);

        // Vertical gap
        Label gridVGapLbl = new Label("Vertical Gap");

        final Slider gridVGapSlider = new Slider();
        gridVGapSlider.setMin(0);
        gridVGapSlider.setMax(100);
        gridVGapSlider.setValue(5);
        gridVGapSlider.setShowTickLabels(true);
        gridVGapSlider.setShowTickMarks(true);
        gridVGapSlider.setMinorTickCount(1);
        gridVGapSlider.setBlockIncrement(5);

        gridVGapSlider.valueProperty().addListener((ObservableValue<? extends Number> ov, Number oldVal, Number newVal) -> {
            targetGridPane.setVgap((double) newVal);
        });

       // vgap label
       GridPane.setHalignment(gridVGapLbl, HPos.RIGHT);
       add(gridVGapLbl, 0, 4);

       // vgap slider
       GridPane.setHalignment(gridVGapSlider, HPos.LEFT);
       add(gridVGapSlider, 1, 4);

       // Cell Column
       Label cellCol = new Label("Cell Column");
       final TextField cellColFld = new TextField("0");

       // cell Column label
       GridPane.setHalignment(cellCol, HPos.RIGHT);
       add(cellCol, 0, 5);

       // cell Column field
       GridPane.setHalignment(cellColFld, HPos.LEFT);
       add(cellColFld, 1, 5);

       // Cell Row
       Label cellRowLbl = new Label("Cell Row");
       final TextField cellRowFld = new TextField("0");

       // cell Row label
       GridPane.setHalignment(cellRowLbl, HPos.RIGHT);
       add(cellRowLbl, 0, 6);

       // cell Row field
       GridPane.setHalignment(cellRowFld, HPos.LEFT);
       add(cellRowFld, 1, 6);

       // Horizontal Alignment
       Label hAlignLbl = new Label("Horiz. Align");
       final ChoiceBox hAlignFld = new ChoiceBox(FXCollections.observableArrayList(
            "CENTER", "LEFT", "RIGHT")
       );
       hAlignFld.getSelectionModel().select("LEFT");

       // cell Row label
       GridPane.setHalignment(hAlignLbl, HPos.RIGHT);
       add(hAlignLbl, 0, 7);

       // cell Row field
       GridPane.setHalignment(hAlignFld, HPos.LEFT);
       add(hAlignFld, 1, 7);

       // Vertical Alignment
       Label vAlignLbl = new Label("Vert. Align");
       final ChoiceBox vAlignFld = new ChoiceBox(FXCollections.observableArrayList(
            "BASELINE", "BOTTOM", "CENTER", "TOP")
       );
       vAlignFld.getSelectionModel().select("TOP");
       // cell Row label
       GridPane.setHalignment(vAlignLbl, HPos.RIGHT);
       add(vAlignLbl, 0, 8);

       // cell Row field
       GridPane.setHalignment(vAlignFld, HPos.LEFT);
       add(vAlignFld, 1, 8);

       // Vertical Alignment
       Label cellApplyLbl = new Label("Cell Constraint");
       final Button cellApplyButton = new Button("Apply");
       cellApplyButton.setOnAction((ActionEvent event) -> {
           for (Node child:targetGridPane.getChildren()) {

               int targetColIndx = 0;
               int targetRowIndx = 0;
               try {
                   targetColIndx = Integer.parseInt(cellColFld.getText());
                   targetRowIndx = Integer.parseInt(cellRowFld.getText());
               } catch (NumberFormatException e) {

               }
               System.out.println("child = " + child.getClass().getSimpleName());
               int col = GridPane.getColumnIndex(child);
               int row = GridPane.getRowIndex(child);
               if (col == targetColIndx && row == targetRowIndx) {
                   GridPane.setHalignment(child, HPos.valueOf(hAlignFld.getSelectionModel().getSelectedItem().toString()));
                   GridPane.setValignment(child, VPos.valueOf(vAlignFld.getSelectionModel().getSelectedItem().toString()));
               }
           }
        });

       // cell Row label
       GridPane.setHalignment(cellApplyLbl, HPos.RIGHT);
       add(cellApplyLbl, 0, 9);

       // cell Row field
       GridPane.setHalignment(cellApplyButton, HPos.LEFT);
       add(cellApplyButton, 1, 9);

    }
}

图 15-7 显示了一个应用,左边是 GridPane 属性控制面板,右边是目标表单。

A323910_3_En_15_Fig7_HTML.jpg

图 15-7。通过网格操纵布局

它是如何工作的

表单设计器应用允许用户使用左侧的 GridPane 属性控制面板来调整属性。从左侧控制面板调整属性时,右侧的目标表单将被动态操作。当创建这样的应用时,您将把控件绑定到目标表单(GridPane)上的各种属性。这个设计器应用基本上分为三个类:ManipulatingLayoutViaGrids、MyForm 和 GridPaneControlPanel。ManipulatingLayoutViaGrids 类是将要启动的主要应用。MyForm 是将被操作的目标表单,GridPaneControlPanel 是网格属性控制面板,它具有绑定到目标表单的网格窗格属性的 UI 控件。

首先创建应用的主启动点(ManipulatingLayoutViaGrids)。该类负责创建一个拆分窗格(split pane ),该窗格在右侧设置目标表单,并实例化一个显示在左侧的 GridPaneControlPanel。要实例化 GridPaneControlPanel,必须将想要操作的目标表单传入构造函数。我将进一步讨论这一点,但可以说 GridPaneControlPanel 构造函数将把它的控件连接到目标表单的属性。

接下来,创建一个名为 my form 的虚拟表单。这是属性控制面板将操作的目标表单。这里,注意 MyForm 扩展了 GridPane。在 MyForm 的构造函数中,创建并添加要放入表单的控件(GridPane)。

要了解更多关于 GridPane 的知识,请参考食谱 15-8。以下代码是由表单设计器应用操作的目标表单:

/**
 * MyForm is a form to be manipulated by the user.
 * @author cdea
 */
public class MyForm extends GridPane{
    public MyForm() {

        setPadding(new Insets(5));
        setHgap(5);
        setVgap(5);

        Label fNameLbl = new Label("First Name");
        TextField fNameFld = new TextField();
        Label lNameLbl = new Label("Last Name");
        TextField lNameFld = new TextField();
        Label ageLbl = new Label("Age");
        TextField ageFld = new TextField();

        Button saveButt = new Button("Save");

        // First name label
        GridPane.setHalignment(fNameLbl, HPos.RIGHT);
        add(fNameLbl, 0, 0);
    //... The rest of the form code

要操作目标表单,您需要创建一个网格属性控制面板(GridPaneControlPanel)。该类负责将目标窗体的网格窗格属性绑定到允许用户使用键盘和鼠标调整值的 UI 控件。正如您在第十四章中了解到的,在 Recipe 14-9 中,您可以将值与 JavaFX 属性绑定。但是,除了直接绑定值之外,您还可以在属性发生更改时得到通知。

可以添加到属性中的另一个功能是更改侦听器。Java FX Java FX . beans . value . change listeners 类似于 Java swing 的属性更改支持(Java . beans . propertychangelister)。类似地,当 bean 的属性值发生变化时,您会希望得到通知。变更侦听器通过使新旧值对开发人员可用来拦截变更。该示例通过为切换按钮创建一个 JavaFXchange 侦听器来打开或关闭网格线,从而开始这个过程。当用户与切换按钮交互时,更改侦听器将简单地更新目标网格窗格的 gridlinesVisible 属性。因为切换按钮的(ToggleButton) selected 属性是一个布尔值,所以实例化一个 ChangeListener 类,其形式类型参数为 Boolean。您还会注意到 lambda expression change listener 实现,其中它的入站参数将匹配在实例化 ChangeListener 时指定的通用形式类型参数。当属性更改事件发生时,更改侦听器将使用新值调用目标网格窗格上的 setGridLinesVisible(),并更新切换按钮的文本。以下代码片段显示了添加到 ToggleButton 的 ChangeListener :

gridLinesToggle.selectedProperty().addListener(
        (ObservableValue<? extends Boolean> ov,
                Boolean oldValue, Boolean newVal) -> {
    targetGridPane.setGridLinesVisible(newVal);
    gridLinesToggle.setText(newVal ? "On" : "Off");
});

接下来,将一个更改侦听器应用到一个 slider 控件,该控件允许用户调整目标网格窗格的顶部填充。要为滑块创建一个 change listener,需要实例化一个 ChangeListener 。同样,您将使用一个 lambda 表达式,其签名与其形式类型参数号相同。当发生更改时,滑块的值用于创建 Insets 对象,该对象成为目标网格窗格的新填充。此处显示的是顶部填充和滑块控件的更改监听器:

gridPaddingSlider.valueProperty().addListener((
        ObservableValue<? extends Number> ov, Number oldVal, Number newVal) -> {
    double top1 = targetGridPane.getInsets().getTop();
    double right1 = targetGridPane.getInsets().getRight();
    double bottom1 = targetGridPane.getInsets().getBottom();
    double left1 = targetGridPane.getInsets().getLeft();
    Insets newInsets = new Insets((double) newVal, right1, bottom1, left1);
    targetGridPane.setPadding(newInsets);
});

因为处理左填充、水平间距和垂直间距的其他滑块控件的实现实际上与前面提到的顶部填充滑块控件相同,所以您可以快进到单元格约束控件。

您想要操作的网格控制面板属性的最后一部分是目标网格窗格的单元格约束。为简洁起见,该示例只允许用户在 GridPane 的单元格内设置组件的对齐方式。要查看更多要修改的属性,请参考 javafx.scene.layout.GridPane 上的 Javadoc。图 15-8 描述了单个单元格的单元格约束设置。一个例子是在目标网格窗格上左对齐标签年龄。因为单元格是零相关的,所以您将在单元格列字段中输入 0 ,在单元格行字段中输入 2。接下来,选择下拉框 Horiz。向左对齐。对设置满意后,点按“应用”。图 15-9 显示水平左对齐的年龄标签控件。要实现这一更改,请创建一个 lambda 表达式,为应用按钮的 onAction 属性实现 EventHandler < ActionEvent >。在 lambda 表达式中,迭代目标网格窗格拥有的节点子级,以确定它是否是指定的单元格。一旦确定了指定的单元格和子节点,就会应用对齐方式。下面的代码显示了当按下“应用”按钮时应用单元格约束的 EventHandler:

A323910_3_En_15_Fig8_HTML.jpg

图 15-8。单元格约束

A323910_3_En_15_Fig9_HTML.jpg

图 15-9。目标网格窗格
cellApplyButton.setOnAction((ActionEvent event) -> {
    for (Node child:targetGridPane.getChildren()) {

        int targetColIndx = 0;
        int targetRowIndx = 0;
        try {
            targetColIndx = Integer.parseInt(cellColFld.getText());
            targetRowIndx = Integer.parseInt(cellRowFld.getText());
        } catch (NumberFormatException e) {

        }
        System.out.println("child = " + child.getClass().getSimpleName());
        int col = GridPane.getColumnIndex(child);
        int row = GridPane.getRowIndex(child);
        if (col == targetColIndx && row == targetRowIndx) {
            GridPane.setHalignment(child, HPos.valueOf(hAlignFld.getSelectionModel().getSelectedItem().toString()));
            GridPane.setValignment(child, VPos.valueOf(vAlignFld.getSelectionModel().getSelectedItem().toString()));
        }
    }
 });

图 15-8 描绘了单元格约束网格控制面板部分,它将控件左对齐单元格第 0 列和单元格第 2 行。

图 15-9 描绘了目标网格窗格,网格线打开,年龄标签在单元格第 0 列和单元格第 2 行水平左对齐。

15-5.用 CSS 增强界面

问题

您希望改变 GUI 界面的外观和感觉。

解决办法

将 JavaFX 的 CSS 样式应用于图形节点。下面的代码演示了在图形节点上使用 CSS 样式。代码创建了五个主题:摩德纳、里海、控制样式 1、控制样式 2 和天空。每个主题都是使用 CSS 定义的,并影响对话框的外观。按照代码,您可以看到该对话框的两种不同版本:

package org.java9recipes.chapter15.recipe15_05;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
 * Recipe 15-5:  Enhancing with CSS
 * @author cdea
 * Update: J Juneau
 */
public class EnhancingWithCss extends Application {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

            primaryStage.setTitle("Chapter 15-5 Enhancing with CSS ");
            Group root = new Group();
            final Scene scene = new Scene(root, 640, 480, Color.BLACK);
            MenuBar menuBar = new MenuBar();
            Menu menu = new Menu("Look and Feel");

            //  Modena Look and Feel
            MenuItem modenaLnf = new MenuItem("Modena");
            modenaLnf.setOnAction(enableCss(STYLESHEET_MODENA,scene));
            menu.getItems().add(modenaLnf);

            // Old default, Caspian Look and Feel
            MenuItem caspianLnf = new MenuItem("Caspian");
            caspianLnf.setOnAction(enableCss(STYLESHEET_CASPIAN, scene));

            menu.getItems().add(caspianLnf);

            menu.getItems().add(createMenuItem("Control Style 1", "controlStyle1.css", scene));
            menu.getItems().add(createMenuItem("Control Style 2", "controlStyle2.css", scene));
             menu.getItems().add(createMenuItem("Sky", "sky.css", scene));       

            menuBar.getMenus().add(menu);
            // stretch menu
            menuBar.prefWidthProperty().bind(primaryStage.widthProperty());

            // Left and right split pane
            SplitPane splitPane = new SplitPane();
            splitPane.prefWidthProperty().bind(scene.widthProperty());
            splitPane.prefHeightProperty().bind(scene.heightProperty());

            // Form on the right
            GridPane rightGridPane = new MyForm();

            GridPane leftGridPane = new GridPaneControlPanel(rightGridPane);
            VBox leftArea = new VBox(10);
            leftArea.getChildren().add(leftGridPane);

            HBox hbox = new HBox();
            hbox.getChildren().add(splitPane);
            VBox vbox = new VBox();
            vbox.getChildren().add(menuBar);
            vbox.getChildren().add(hbox);
            root.getChildren().add(vbox);
            splitPane.getItems().addAll(leftArea, rightGridPane);

            primaryStage.setScene(scene);

            primaryStage.show();

    }

    protected final MenuItem createMenuItem(String label, String css, final Scene scene){
        MenuItem menuItem = new MenuItem(label);
        ObservableList<String> cssStyle = loadSkin(css);
        menuItem.setOnAction(skinForm(cssStyle, scene));
        return menuItem;
    }

    protected final ObservableList<String> loadSkin(String cssFileName) {
        ObservableList<String> cssStyle = FXCollections.observableArrayList();
        cssStyle.addAll(getClass().getResource(cssFileName).toExternalForm());
        return cssStyle;
    }

    protected final EventHandler<ActionEvent> skinForm
        (final ObservableList<String> cssStyle, final Scene scene) {
        return (ActionEvent event) -> {
            scene.getStylesheets().clear();
            scene.getStylesheets().addAll(cssStyle);
        };
    }

    protected final EventHandler<ActionEvent> enableCss(String style, final Scene scene){
        return (ActionEvent event) -> {

            scene.getStylesheets().clear();
            setUserAgentStylesheet(style);
        };
    }

}

图 15-10 描绘了标准的 JavaFX Modena 外观和感觉(主题)。

A323910_3_En_15_Fig10_HTML.jpg

图 15-10。摩德纳外观和感觉

图 15-11 描绘了控制风格 1 的外观和感觉(主题)。

A323910_3_En_15_Fig11_HTML.jpg

图 15-11。控制样式 1 外观和感觉

它是如何工作的

JavaFX 能够将 CSS 样式应用于场景图及其节点,就像浏览器将 CSS 样式应用于 HTML 文档对象模型(DOM)中的元素一样。在本菜谱中,您将使用 JavaFX 样式属性来设置用户界面的外观。您基本上使用菜谱的 UI 来应用各种外观和感觉。为了展示可用的皮肤,菜单选项允许用户选择应用于 UI 的外观。

在讨论 CSS 样式属性之前,先看看如何加载要应用于 JavaFX 应用的 CSS 样式。示例中的应用使用菜单项来允许用户选择自己喜欢的外观。创建菜单项时,您将创建一个方便的方法来构建一个菜单项,该菜单项通过 lambda 表达式加载指定的 CSS 和 EventHandler 操作,以将所选的 CSS 样式应用于当前 UI。默认情况下会加载 Modena 外观。通过将各自的样式表传递给 setUserAgentStylesheet()方法,可以应用不同的外观。例如,要加载 Caspian 外观,只需将常量 STYLESHEET_CASPIAN 传递给 setUserAgentStylesheet()方法。以下代码显示了如何创建这些菜单项:

MenuItem caspianLnf = new MenuItem("Caspian");
caspianLnf.setOnAction(skinForm(caspian, scene));

接下来显示的是添加一个包含 Sky Look and Feel CSS 样式的菜单项的代码,它可以应用于当前的 UI。

// Modena Look and Feel
MenuItem modenaLnf = new MenuItem("Modena");
modenaLnf.setOnAction(enableCss(STYLESHEET_MODENA,scene));
menu.getItems().add(modenaLnf);

setOnAction()方法调用名为 enableCss()的方法,该方法采用样式表和当前场景。enableCss()的代码如下:

protected final EventHandler<ActionEvent> enableCss(String style, final Scene scene){
        return (ActionEvent event) -> {

            scene.getStylesheets().clear();
            setUserAgentStylesheet(style);
        };
    }

对于不属于默认 JavaFX 发行版的其他 CSS 样式,菜单项的创建略有不同。这是一个利用前面讨论过的便利方法的代码示例。

menu.getItems().add(createMenuItem("Control Style 1", "controlStyle1.css", scene));

调用 createMenuItem()方法还将调用另一个方便的方法来加载名为 loadSkin()的 CSS 文件。它还将通过调用 skinForm()方法,使用适当的 EventHandler 设置菜单项的 onAction 属性。概括地说,loadSkin 负责加载 CSS 文件,skinForm()方法的工作是将皮肤应用到 UI 应用上。此处显示了构建将 CSS 样式应用于 UI 应用的菜单项的便利方法:

    protected final MenuItem createMenuItem(String label, String css, final Scene scene){
        MenuItem menuItem = new MenuItem(label);
        ObservableList<String> cssStyle = loadSkin(css);
        menuItem.setOnAction(skinForm(cssStyle, scene));
        return menuItem;
    }

    protected final ObservableList<String> loadSkin(String cssFileName) {
        ObservableList<String> cssStyle = FXCollections.observableArrayList();
        cssStyle.addAll(getClass().getResource(cssFileName).toExternalForm());
        return cssStyle;
    }

    protected final EventHandler<ActionEvent> skinForm
    (final ObservableList<String> cssStyle, final Scene scene) {
    return (ActionEvent event) -> {
        scene.getStylesheets().clear();
        scene.getStylesheets().addAll(cssStyle);
    };
}
注意

要运行这个方法,请确保 CSS 文件位于编译的类区域。当资源文件与加载它们的编译后的类文件放在同一个目录(包)中时,可以很容易地加载它们。CSS 文件与此代码示例文件放在一起。在 NetBeans 中,您可以选择清理并构建项目,也可以将文件复制到您的类的构建区域。

现在您已经知道了如何加载 CSS 样式,让我们来谈谈 JavaFX CSS 选择器和样式属性。像 CSS 样式表一样,场景图中也有与节点对象相关联的选择器或样式类。所有场景图节点都有一个名为 setStyle()的方法,该方法应用样式属性,这些属性可能会更改节点的背景色、边框、描边等。因为所有图形节点都从 Node 类扩展而来,所以派生类将能够继承相同的样式属性。了解节点类型的继承层次非常重要,因为节点的类型将决定您可以影响的样式属性的类型。例如,矩形从形状延伸,形状从节点延伸。继承不包括-fx-border-style,它是从 Region 扩展的节点的一部分。根据节点的类型,可以设置的样式是有限的。要查看所有样式选择器的完整列表,请参考 JavaFX CSS 参考指南:

http://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html

所有 JavaFX 样式属性都带有前缀-fx-。例如,所有节点都具有影响不透明度的样式属性,该属性为-fx-opacity。以下是设置 Java FX Java FX . scene . control . labels 和 javafx.scene.control.Buttons 样式的选择器:

.label {
    -fx-text-fill: rgba(17, 145, 213);
    -fx-border-color: rgba(255, 255, 255, .80);
    -fx-border-radius: 8;
    -fx-padding: 6 6 6 6;
    -fx-font: bold italic 20pt "LucidaBrightDemiBold";

}
.button{
    -fx-text-fill: rgba(17, 145, 213);
    -fx-border-color: rgba(255, 255, 255, .80);
    -fx-border-radius: 8;
    -fx-padding: 6 6 6 6;
    -fx-font: bold italic 20pt "LucidaBrightDemiBold";

}

摘要

在本章中,我们讨论了与 JavaFX 图形相关的各种主题。我们学习了如何通过开发一个应用来创建图像,该应用允许用户将图像拖放到舞台上,从而创建图像的副本。然后我们介绍了食谱;它支持文本和形状的动画。最后,我们学习了如何利用网格和/或 CSS 来布局应用组件。

十六、使用 JavaFX 的媒体

JavaFX 提供了能够播放音频和视频的富媒体 API。媒体 API 允许开发人员将音频和视频合并到他们的富客户端应用中。Media API 的一个主要优点是在通过 web 分发媒体内容时它的跨平台能力。对于一系列需要播放多媒体内容的设备(平板电脑、音乐播放器、电视等等),对跨平台 API 的需求是必不可少的。

想象一下在不久的将来,你的电视或墙壁能够以你做梦也想不到的方式与你互动。例如,在观看电影时,您可以选择电影中使用的物品或衣服立即购买,所有这些都可以在您舒适的家中完成。考虑到这种未来,开发人员寻求提高他们基于媒体的应用的交互质量。

在本章中,你将学习如何以互动的方式播放音频和视频。找到您在 JavaFX 第三幕的座位,因为音频和视频占据了中心舞台——如图 16-1 所示。

A323910_3_En_16_Fig1_HTML.jpg

图 16-1。音频和视频

16-1.播放音频

问题

您希望编写一个应用,让您可以听音乐,并通过图形可视化来娱乐。

解决办法

利用下面的类创建一个 MP3 播放器:

  • javafx.scene.media.Media

  • javafx.scene.media.MediaPlayer

  • Java FX . scene . media . audiospectrumlistener

下面的源代码是一个简单 MP3 播放器的实现:

package org.java9recipes.chapter16.recipe16_01;

import java.io.File;
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.media.AudioSpectrumListener;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class PlayingAudio extends Application {

    private MediaPlayer mediaPlayer;
    private Point2D anchorPt;
    private Point2D previousLocation;

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(final Stage primaryStage) {
        primaryStage.setTitle("Chapter 16-1 Playing Audio");
        primaryStage.centerOnScreen();
        primaryStage.initStyle(StageStyle.TRANSPARENT);

        Group root = new Group();
        Scene scene = new Scene(root, 551, 270, Color.rgb(0, 0, 0, 0));

        // application area
        Rectangle applicationArea = new Rectangle();
        applicationArea.setArcWidth(20);
        applicationArea.setArcHeight(20);
        applicationArea.setFill(Color.rgb(0, 0, 0, .80));
        applicationArea.setX(0);
        applicationArea.setY(0);
        applicationArea.setStrokeWidth(2);
        applicationArea.setStroke(Color.rgb(255, 255, 255, .70));

        root.getChildren().add(applicationArea);
        applicationArea.widthProperty().bind(scene.widthProperty());
        applicationArea.heightProperty().bind(scene.heightProperty());

        final Group phaseNodes = new Group();
        root.getChildren().add(phaseNodes);

        // starting initial anchor point
        scene.setOnMousePressed((MouseEvent event) -> {
            anchorPt = new Point2D(event.getScreenX(), event.getScreenY());
        });

        // dragging the entire stage
        scene.setOnMouseDragged((MouseEvent event) -> {
            if (anchorPt != null && previousLocation != null) {
                primaryStage.setX(previousLocation.getX() + event.getScreenX() - anchorPt.getX());
                primaryStage.setY(previousLocation.getY() + event.getScreenY() - anchorPt.getY());
            }
        });

        // set the current location
        scene.setOnMouseReleased((MouseEvent event) -> {
            previousLocation = new Point2D(primaryStage.getX(), primaryStage.getY());
        });

        // Dragging over surface
        scene.setOnDragOver((DragEvent event) -> {
            Dragboard db = event.getDragboard();
            if (db.hasFiles()) {
                event.acceptTransferModes(TransferMode.COPY);
            } else {
                event.consume();
            }
        });

        // Dropping over surface
        scene.setOnDragDropped((DragEvent event) -> {
            Dragboard db = event.getDragboard();
            boolean success = false;
            if (db.hasFiles()) {
                success = true;
                String filePath = null;
                for (File file : db.getFiles()) {
                    filePath = file.getAbsolutePath();
                    System.out.println(filePath);
                }
                // play file
                Media media = new Media(new File(filePath).toURI().toString());

                if (mediaPlayer != null) {
                    mediaPlayer.stop();
                }

                mediaPlayer = new MediaPlayer(media);

                // Maintained Inner Class for Tutorial, could be changed to lambda
                mediaPlayer.setAudioSpectrumListener(new AudioSpectrumListener() {
                    @Override
                    public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases) {
                        phaseNodes.getChildren().clear();
                        int i = 0;
                        int x = 10;
                        int y = 150;
                        final Random rand = new Random(System.currentTimeMillis());
                        for (float phase : phases) {
                            int red = rand.nextInt(255);
                            int green = rand.nextInt(255);
                            int blue = rand.nextInt(255);

                            Circle circle = new Circle(10);
                            circle.setCenterX(x + i);
                            circle.setCenterY(y + (phase * 100));
                            circle.setFill(Color.rgb(red, green, blue, .70));
                            phaseNodes.getChildren().add(circle);
                            i += 5;
                        }
                    }
                });

                mediaPlayer.setOnReady(mediaPlayer::play);
            }

            event.setDropCompleted(success);
            event.consume();
        });

        // create slide controls
        final Group buttonGroup = new Group();

        // rounded rect
        Rectangle buttonArea = new Rectangle();
        buttonArea.setArcWidth(15);
        buttonArea.setArcHeight(20);
        buttonArea.setFill(new Color(0, 0, 0, .55));
        buttonArea.setX(0);
        buttonArea.setY(0);
        buttonArea.setWidth(60);
        buttonArea.setHeight(30);
        buttonArea.setStroke(Color.rgb(255, 255, 255, .70));

        buttonGroup.getChildren().add(buttonArea);
        // stop audio control
        Rectangle stopButton = new Rectangle();
        stopButton.setArcWidth(5);
        stopButton.setArcHeight(5);
        stopButton.setFill(Color.rgb(255, 255, 255, .80));
        stopButton.setX(0);
        stopButton.setY(0);
        stopButton.setWidth(10);
        stopButton.setHeight(10);
        stopButton.setTranslateX(15);
        stopButton.setTranslateY(10);
        stopButton.setStroke(Color.rgb(255, 255, 255, .70));

        stopButton.setOnMousePressed((MouseEvent me) -> {
            if (mediaPlayer != null) {
                mediaPlayer.stop();
            }
        });
        buttonGroup.getChildren().add(stopButton);

        // play control
        final Arc playButton = new Arc();
        playButton.setType(ArcType.ROUND);
        playButton.setCenterX(12);
        playButton.setCenterY(16);
        playButton.setRadiusX(15);
        playButton.setRadiusY(15);
        playButton.setStartAngle(180 - 30);
        playButton.setLength(60);
        playButton.setFill(new Color(1, 1, 1, .90));
        playButton.setTranslateX(40);

        playButton.setOnMousePressed((MouseEvent me) -> {
            mediaPlayer.play();
        });

        // pause control
        final Group pause = new Group();
        final Circle pauseButton = new Circle();
        pauseButton.setCenterX(12);
        pauseButton.setCenterY(16);
        pauseButton.setRadius(10);
        pauseButton.setStroke(new Color(1, 1, 1, .90));
        pauseButton.setTranslateX(30);

        final Line firstLine = new Line();
        firstLine.setStartX(6);
        firstLine.setStartY(16 - 10);
        firstLine.setEndX(6);
        firstLine.setEndY(16 - 2);
        firstLine.setStrokeWidth(3);
        firstLine.setTranslateX(34);
        firstLine.setTranslateY(6);
        firstLine.setStroke(new Color(1, 1, 1, .90));

        final Line secondLine = new Line();
        secondLine.setStartX(6);
        secondLine.setStartY(16 - 10);
        secondLine.setEndX(6);
        secondLine.setEndY(16 - 2);
        secondLine.setStrokeWidth(3);
        secondLine.setTranslateX(38);
        secondLine.setTranslateY(6);
        secondLine.setStroke(new Color(1, 1, 1, .90));

        pause.getChildren().addAll(pauseButton, firstLine, secondLine);

        pause.setOnMousePressed((MouseEvent me) -> {
            if (mediaPlayer != null) {
                buttonGroup.getChildren().remove(pause);
                buttonGroup.getChildren().add(playButton);
                mediaPlayer.pause();
            }
        });

        playButton.setOnMousePressed((MouseEvent me) -> {
            if (mediaPlayer != null) {
                buttonGroup.getChildren().remove(playButton);
                buttonGroup.getChildren().add(pause);
                mediaPlayer.play();
            }
        });

        buttonGroup.getChildren().add(pause);
        // move button group when scene is resized
        buttonGroup.translateXProperty().bind(scene.widthProperty().subtract(buttonArea.getWidth() + 6));
        buttonGroup.translateYProperty().bind(scene.heightProperty().subtract(buttonArea.getHeight() + 6));
        root.getChildren().add(buttonGroup);

        // close button
        final Group closeApp = new Group();
        Circle closeButton = new Circle();
        closeButton.setCenterX(5);
        closeButton.setCenterY(0);
        closeButton.setRadius(7);
        closeButton.setFill(Color.rgb(255, 255, 255, .80));

        Node closeXmark = new Text(2, 4, "X");
        closeApp.translateXProperty().bind(scene.widthProperty().subtract(15));
        closeApp.setTranslateY(10);
        closeApp.getChildren().addAll(closeButton, closeXmark);
        closeApp.setOnMouseClicked((MouseEvent event) -> {
            Platform.exit();
        });

        root.getChildren().add(closeApp);

        primaryStage.setScene(scene);
        primaryStage.show();
        previousLocation = new Point2D(primaryStage.getX(), primaryStage.getY());

    }
}

图 16-2 显示了一个带有可视化效果的 JavaFX MP3 播放器。

A323910_3_En_16_Fig2_HTML.jpg

图 16-2。JavaFX MP3 播放器

它是如何工作的

在您开始之前,我将讨论如何操作所创建的 MP3 播放器应用的说明。用户可以将音频文件拖放到应用区域进行播放。位于应用右下角的是停止、暂停和恢复播放音频媒体的按钮。(按钮控制如图 16-2 所示。)当音乐播放时,用户还会注意到随机的彩色球随着音乐来回跳动。一旦用户听完音乐,他们可以通过点击右上角的白色圆形关闭按钮来退出应用。

它类似于食谱 15-1,其中您学习了如何使用拖放桌面隐喻将文件加载到 JavaFX 应用中。然而,用户访问的不是图像文件,而是音频文件。JavaFX 目前支持以下音频文件格式:. mp3,.wav,还有. aiff。

遵循相同的外观和感觉,您将使用与配方 15-1 相同的样式。在这个方法中,您将按钮控件修改为类似于按钮,类似于许多媒体播放器应用。当按下暂停按钮时,它将暂停音频媒体的播放并切换到播放按钮控制,从而允许用户继续。作为一个额外的奖励,MP3 播放器将显示为一个不规则形状的半透明无边框窗口,也可以用鼠标在桌面上拖动。现在您已经知道了音乐播放器将如何操作,让我们浏览一下代码。

首先,您需要创建在应用的生命周期内维护状态信息的实例变量。表 16-1 描述了该音乐播放器应用中使用的所有实例变量。第一个变量是对媒体播放器(media player)对象的引用,该对象将与包含音频文件的媒体对象一起创建。接下来,创建一个 anchorPt 变量,用于在用户开始在屏幕上拖动窗口时保存鼠标按下的起始坐标。在鼠标拖动操作期间计算应用窗口的左上边界时,previousLocation 变量将包含前一个窗口的屏幕 X 和 Y 坐标。

表 16-1。MP3 播放器应用实例变量
|

可变的

|

数据类型

|

例子

|

描述

|
| --- | --- | --- | --- |
| 媒体播放机 | 媒体播放机 | 不适用的 | 播放音频和视频的媒体播放器控件 |
| anchorPt | 点 2D | One hundred thousand one hundred | 用户开始拖动窗口的坐标 |
| 先前位置 | 点 2D | 0,0 | 舞台上一个坐标的左上角;协助拖动窗口 |

表 16-1 列出了 MP3 播放器应用的实例变量。

在前面关于 GUI 的章节中,你看到了 GUI 应用通常包含一个标题栏和围绕场景的窗口边框。在这里,我想通过向您展示如何创建不规则形状的半透明窗口,从而使事物看起来更加时尚或现代,来提高标准。当您开始创建媒体播放器时,您会注意到在 start()方法中,您通过使用 StageStyle.TRANSPARENT 初始化样式来准备 Stage 对象。透明,窗口将不被修饰,整个窗口区域的不透明值设置为零(不可见)。以下代码向您展示了如何创建一个没有标题栏或窗口边框的透明窗口:

primaryStage.initStyle(StageStyle.TRANSPARENT);

使用不可见的舞台,您可以创建一个圆角矩形区域,该区域将成为应用的表面或主要内容区域。接下来,注意绑定到场景对象的矩形的宽度和高度,以防窗口被调整大小。因为窗口不会被调整大小,所以绑定是不必要的(然而,在方法 16-2 中,当你提供放大视频屏幕以呈现全屏模式的能力时,它是需要的)。

在创建一个黑色、半透明的圆角矩形区域(applicationArea)之后,您将创建一个简单的 Group 对象来保存所有随机着色的圆形节点,这些节点将在播放音频时展示图形可视化效果。稍后,您将看到如何使用 AudioSpectrumListener 根据声音信息更新 phaseNodes (Group)变量。

接下来,将 EventHandler 实例添加到 Scene 对象(该示例使用 lambda 表达式)中,以便在用户在屏幕上拖动窗口时监视鼠标事件。这个场景中的第一个事件是鼠标按下,这将把光标的当前(X,Y)坐标保存到变量 anchorPt 中。以下代码将 EventHandler 添加到场景的 mouse-press 属性中:

// starting initial anchor point
scene.setOnMousePressed((MouseEvent event) -> {
    anchorPt = new Point2D(event.getScreenX(), event.getScreenY());
});

实现鼠标按下事件处理程序后,可以为场景的鼠标拖动属性创建一个事件处理程序。鼠标拖动事件处理程序将根据前一个窗口的位置(左上角)以及 anchorPt 变量,动态更新和定位应用窗口(舞台)。这里显示的是负责场景对象上鼠标拖动事件的事件处理程序:

// dragging the entire stage
scene.setOnMouseDragged((MouseEvent event) -> {
    if (anchorPt != null && previousLocation != null) {
        primaryStage.setX(previousLocation.getX() + event.getScreenX() - anchorPt.getX());
        primaryStage.setY(previousLocation.getY() + event.getScreenY() - anchorPt.getY());
    }
});

您将需要处理鼠标释放事件来执行操作。一旦释放鼠标,事件处理程序将为随后的鼠标拖动事件更新 previousLocation 变量,以在屏幕上移动应用窗口。以下代码片段更新了 previousLocation 变量:

// set the current location
scene.setOnMouseReleased((MouseEvent event) -> {
    previousLocation = new Point2D(primaryStage.getX(), primaryStage.getY());
});

接下来,您将实现拖放场景来从文件系统加载音频文件(使用文件管理器)。当处理拖放场景时,它类似于配方 15-1,其中您创建了一个 EventHandler 来处理 DragEvents。您将从主机文件系统加载音频文件,而不是加载图像文件。为了简洁起见,我只提到拖放事件处理程序的代码行。一旦音频文件可用,您将通过将文件作为 URI 传入来创建媒体对象。以下代码片段是如何创建媒体对象的:

Media media = new Media(new File(filePath).toURI().toString());

一旦创建了媒体对象,就必须创建一个 MediaPlayer 实例来播放声音文件。Media 和 MediaPlayer 对象都是不可变的,这就是为什么每次用户将文件拖动到应用中时,都会创建每个对象的新实例。接下来,您将检查前一个实例的实例变量 mediaPlayer,以确保它在创建新的 MediaPlayer 实例之前已经停止。以下代码检查要停止的前一个媒体播放器:

  if (mediaPlayer != null) {
    mediaPlayer.stop();
  }

因此,这里是您创建 MediaPlayer 实例的地方。MediaPlayer 对象负责控制媒体对象的播放。请注意,MediaPlayer 在播放、暂停和停止媒体方面将声音或视频媒体视为相同。创建媒体播放器时,需要指定 media 和 audioSpectrumListener 属性方法。将 autoPlay 属性设置为 true 将在加载音频媒体后立即播放它。在 MediaPlayer 实例上最后要指定的是 AudioSpectrumListener。你说,这种类型的听众到底是什么样的?根据 Javadoc,它是一个接收音频频谱定期更新的观察者。通俗地说,就是音频媒体的声音数据,比如音量、节奏等等。要创建 AudioSpectrumListener 的实例,需要创建一个内部类来重写 spectrumDataUpdate()方法。你也可以在这里使用 lambda 表达式;该示例使用内部类来更好地了解功能。表 16-2 列出了音频频谱监听器方法的所有入站参数。更多详情请参考docs . Oracle . com/javase/8/Java FX/API/Java FX/scene/media/audiospectrumlistener . html的 Javadoc。

表 16-2。AudioSpectrumListener 的方法 spectrumDataUpdate()入站参数
|

可变的

|

数据类型

|

例子

|

描述

|
| --- | --- | --- | --- |
| 时间戳 | 两倍 | 2.4261 | 事件发生的时间(秒) |
| 期间 | 两倍 | Zero point one | 计算光谱的持续时间(秒) |
| 重要 | 浮动[] | -50.474335 | 浮点值数组,以分贝表示每个波段的频谱幅度(非正浮点值) |
| 阶段 | 浮动[] | 1.2217305 | 表示每个波段相位的浮点值数组 |

在该示例中,基于可变相位(浮动数组)创建、定位和放置随机着色的圆形节点。要绘制每个彩色圆,圆的中心 X 增加 5 个像素,圆的中心 Y 加上每个相位值乘以 100。此处显示的是绘制每个随机彩色圆圈的代码片段:

circle.setCenterX(x + i);
circle.setCenterY(y + (phase * 100));
... // setting the circle
i+=5;

以下是 AudioSpectrumListener 的内部类实现:

new AudioSpectrumListener() {
    @Override
    public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases) {

       phaseNodes.getChildren().clear();
       int i = 0;
       int x = 10;
       int y = 150;
       final Random rand = new Random(System.currentTimeMillis());
       for(float phase:phases) {
       int red = rand.nextInt(255);
       int green = rand.nextInt(255);
       int blue = rand.nextInt(255);

         Circle circle = new Circle(10);
         circle.setCenterX(x + i);
         circle.setCenterY(y + (phase * 100));
         circle.setFill(Color.rgb(red, green, blue, .70));
         phaseNodes.getChildren().add(circle);
         i+=5;
      }

    }
 };

一旦创建了媒体播放器,就可以创建一个 java.lang.Runnable 来设置 onReady 属性,以便在媒体处于就绪状态时调用。一旦实现了 ready 事件,run()方法将调用媒体播放器对象的 play()方法来开始播放音频。拖放序列完成后,通过调用事件的值为 true 的 setDropCompleted()方法来通知拖放系统。下面的代码片段演示了如何使用方法引用实现 Runnable,以便在媒体播放器处于就绪状态时立即启动媒体播放器:

   mediaPlayer.setOnReady(mediaPlayer::play);

最后,用 JavaFX 形状创建按钮来表示停止、播放、暂停和关闭按钮。创建形状或自定义节点时,可以向节点添加事件处理程序,以便响应鼠标单击。尽管在 JavaFX 中有一些高级的方法来构建自定义控件,但是本例使用了简单的矩形、弧形、圆形和线条来构建自定义的按钮图标。要查看创建自定义控件的更高级的方法,请参考 Skinnable API 上的 Javadoc 或食谱 16-5。要为鼠标按压附加事件处理程序,只需通过传入 EventHandler 实例来调用 setOnMousePress()方法。下面的代码演示如何添加一个 EventHandler 来响应 stopButton 节点上的鼠标按键:

stopButton.setOnMousePressed((MouseEvent me) -> {
    if (mediaPlayer != null) {
        mediaPlayer.stop();
    }
});

因为所有按钮都使用相同的代码片段,所以只列出了每个按钮将在媒体播放器上执行的方法调用。最后一个按钮 Close 与媒体播放器无关,但它提供了一种退出 MP3 播放器应用的方法。以下操作负责停止、暂停、播放和退出 MP3 播放器应用:

Stop - mediaPlayer.stop();  
Pause - mediaPlayer.pause();
Play - mediaPlayer.play();
Close - Platform.exit();

16-2.播放视频

问题

您希望创建一个应用来查看一个视频文件,该文件带有播放、暂停、停止和搜索的控件。

解决办法

利用以下类创建一个视频媒体播放器应用:

  • javafx.scene.media.Media

  • javafx.scene.media.MediaPlayer

  • javafx.scene.media.MediaView

以下代码是 JavaFX 基本视频播放器的实现:

public void start(final Stage primaryStage) {
    primaryStage.setTitle("Chapter 16-2 Playing Video");
    primaryStage.centerOnScreen();
    primaryStage.initStyle(StageStyle.TRANSPARENT);

    final Group root = new Group();
    final Scene scene = new Scene(root, 540, 300, Color.rgb(0, 0, 0, 0));

    // rounded rectangle with slightly transparent
    Node applicationArea = createBackground(scene);
    root.getChildren().add(applicationArea);

    // allow the user to drag window on the desktop
    attachMouseEvents(scene, primaryStage);

    // allow the user to see the progress of the video playing
    progressSlider = createSlider(scene);
    root.getChildren().add(progressSlider);

    // Dragging over surface
    scene.setOnDragOver((DragEvent event) -> {
        Dragboard db = event.getDragboard();
        if (db.hasFiles() || db.hasUrl() || db.hasString()) {
            event.acceptTransferModes(TransferMode.COPY);
            if (mediaPlayer != null) {
                mediaPlayer.stop();
            }
        } else {
            event.consume();
        }
    });

    // update slider as video is progressing (later removal)
    progressListener = (ObservableValue<? extends Duration> observable, Duration oldValue, Duration newValue) -> {
        progressSlider.setValue(newValue.toSeconds());
    };

    // Dropping over surface
    scene.setOnDragDropped((DragEvent event) -> {
        Dragboard db = event.getDragboard();
        boolean success = false;
        URI resourceUrlOrFile = null;

        // dragged from web browser address line?
        if (db.hasContent(DataFormat.URL)) {
            try {
                resourceUrlOrFile = new URI(db.getUrl());
            } catch (URISyntaxException ex) {
                ex.printStackTrace();
            }
        } else if (db.hasFiles()) {
            // dragged from the file system
            String filePath = null;
            for (File file:db.getFiles()) {
                filePath = file.getAbsolutePath();
            }
            resourceUrlOrFile = new File(filePath).toURI();
            success = true;
        }
        // load media
        Media media = new Media(resourceUrlOrFile.toString());

        // stop previous media player and clean up
        if (mediaPlayer != null) {
            mediaPlayer.stop();
            mediaPlayer.currentTimeProperty().removeListener(progressListener);
            mediaPlayer.setOnPaused(null);
            mediaPlayer.setOnPlaying(null);
            mediaPlayer.setOnReady(null);
        }

        // create a new media player
        mediaPlayer = new MediaPlayer(media);

        // as the media is playing move the slider for progress
        mediaPlayer.currentTimeProperty().addListener(progressListener);

        // play video when ready status
        mediaPlayer.setOnReady(() -> {
            progressSlider.setValue(1);
            progressSlider.setMax(mediaPlayer.getMedia().getDuration().toMillis()/1000);
            mediaPlayer.play();
        });

        // Lazy init media viewer
        if (mediaView == null) {
            mediaView = new MediaView();
            mediaView.setMediaPlayer(mediaPlayer);
            mediaView.setX(4);
            mediaView.setY(4);
            mediaView.setPreserveRatio(true);
            mediaView.setOpacity(.85);
            mediaView.setSmooth(true);

            mediaView.fitWidthProperty().bind(scene.widthProperty().subtract(220));
            mediaView.fitHeightProperty().bind(scene.heightProperty().subtract(30));

            // make media view as the second node on the scene.
            root.getChildren().add(1, mediaView);
        }

        // sometimes loading errors occur, print error when this happens
        mediaView.setOnError((MediaErrorEvent event1) -> {
            event1.getMediaError().printStackTrace();
        });

        mediaView.setMediaPlayer(mediaPlayer);

        event.setDropCompleted(success);
        event.consume();
    });

    // rectangular area holding buttons
    final Group buttonArea = createButtonArea(scene);

    // stop button will stop and rewind the media
    Node stopButton = createStopControl();

    // play button can resume or start a media
    final Node playButton = createPlayControl();

    // pause media play
    final Node pauseButton = createPauseControl();

    stopButton.setOnMousePressed((MouseEvent me) -> {
        if (mediaPlayer!= null) {
            buttonArea.getChildren().removeAll(pauseButton, playButton);
            buttonArea.getChildren().add(playButton);
            mediaPlayer.stop();
        }
    });
    // pause media and swap button with play button
    pauseButton.setOnMousePressed((MouseEvent me) -> {
        if (mediaPlayer!=null) {
            buttonArea.getChildren().removeAll(pauseButton, playButton);
            buttonArea.getChildren().add(playButton);
            mediaPlayer.pause();
            paused = true;
        }
    });

    // play media and swap button with pause button
    playButton.setOnMousePressed((MouseEvent me) -> {
        if (mediaPlayer != null) {
            buttonArea.getChildren().removeAll(pauseButton, playButton);
            buttonArea.getChildren().add(pauseButton);
            paused = false;
            mediaPlayer.play();
        }
    });

    // add stop button to button area
    buttonArea.getChildren().add(stopButton);

    // set pause button as default
    buttonArea.getChildren().add(pauseButton);

    // add buttons
    root.getChildren().add(buttonArea);

    // create a close button
    Node closeButton= createCloseButton(scene);
    root.getChildren().add(closeButton);

    primaryStage.setOnShown((WindowEvent we) -> {
        previousLocation = new Point2D(primaryStage.getX(), primaryStage.getY());
    });

    primaryStage.setScene(scene);
    primaryStage.show();

}

下面是 attachMouseEvents()方法,该方法向场景添加一个 EventHandler,以便视频播放器可以进入全屏模式。

private void attachMouseEvents(Scene scene, final Stage primaryStage) {

    // Full screen toggle
    scene.setOnMouseClicked((MouseEvent event) -> {
        if (event.getClickCount() == 2) {
            primaryStage.setFullScreen(!primaryStage.isFullScreen());
        }
    });
        ... // the rest of the EventHandlers
}

下面的方法创建一个带有 ChangeListener 的 slider 控件,使用户能够在视频中向前和向后搜索:

private Slider createSlider(Scene scene) {
    Slider slider = new Slider();
    slider.setMin(0);
    slider.setMax(100);
    slider.setValue(1);
    slider.setShowTickLabels(true);
    slider.setShowTickMarks(true);

    slider.valueProperty().addListener((ObservableValue<? extends Number> observable,
            Number oldValue, Number newValue) -> {
        if (paused) {
            long dur = newValue.intValue() * 1000;
            mediaPlayer.seek(new Duration(dur));
        }
    });

    slider.translateYProperty().bind(scene.heightProperty().subtract(30));
    return slider;
}

图 16-3 描述了带有滑块控件的 JavaFX 基本视频播放器。

A323910_3_En_16_Fig3_HTML.jpg

图 16-3。JavaFX 基本视频播放器

它是如何工作的

要创建一个视频播放器,你将通过重用相同的应用特性,如拖放文件、媒体按钮控件等,来模拟与配方 16-1 中的例子相似的应用。为了清楚起见,我采用了前面的方法,将大部分 UI 代码移到了方便的函数中,这样您将能够专注于媒体 API,而不会迷失在 UI 代码中。本章的其余方法包括向本方法中创建的 JavaFX basic media player 添加简单的功能。也就是说,下面食谱中的代码片段将会很简短,只包含每个新特性所必需的代码。

值得注意的是,JavaFX 媒体播放器支持各种媒体格式。支持的格式如下:

  • 伊法夫

  • FXM, FLV

  • HLS (*)

  • MP3 文件

  • MP4

  • 声音资源文件

有关支持的媒体类型的完整摘要,请参见位于docs . Oracle . com/javase/8/Java FX/API/Java FX/scene/media/package-summary . html的在线文档。

就像上一个菜谱中创建的音频播放器一样,JavaFX 基本视频播放器具有相同的基本媒体控件,包括停止、暂停和播放。除了这些简单的控件,您还添加了新的功能,如搜索和全屏模式。

播放视频时,您需要一个视图区域(javafx.scene.media.MediaView)来显示它。您还可以创建一个滑块控件来监控视频的进度,该控件位于图 16-3 所示应用的左下方。滑块控件允许用户在视频中向后和向前搜索。最后一个额外的功能是通过双击应用窗口使视频全屏显示。要恢复窗口,用户重复双击或按 Escape 键。

为了快速入门,让我们直接进入代码。在 start()方法中设置舞台后,通过调用 createBackground()方法(applicationArea)创建一个黑色半透明背景。接下来,调用 attachMouseEvents()方法来设置 EventHandlers,以便用户能够在桌面上拖动应用窗口。附加到场景的另一个 EventHandler 将允许用户切换到全屏模式。条件用于检查应用窗口中的双击,以便调用全屏模式。执行双击后,将调用 Stage 的方法 setFullScreen(),调用的布尔值与当前设置的值相反。这里显示的是使窗口进入全屏模式所需的代码:

// Full screen toggle
scene.setOnMouseClicked((MouseEvent event) -> {
    if (event.getClickCount() == 2) {
        primaryStage.setFullScreen(!primaryStage.isFullScreen());
    }
});

当您继续 start()方法中的步骤时,通过调用方便的方法 createSlider()创建一个 slider 控件。createSlider()方法实例化一个 Slider 控件,并添加一个 ChangeListener 以在视频播放时移动滑块。每当滑块的值发生变化时,就会调用 ChangeListener 的 changed()方法。调用 changed()方法后,您将有机会看到旧值和新值。以下代码创建了一个 ChangeListener,用于在视频播放时更新滑块:

// update slider as video is progressing (later removal)
progressListener = (ObservableValue<? extends Duration> observable,
                   Duration oldValue, Duration newValue) -> {
    progressSlider.setValue(newValue.toSeconds());
};

在创建进度监听器(progress listener)之后,需要为场景创建拖放的 EventHandler。目标是确定在用户可以移动滑块之前是否按下了暂停按钮。一旦确定了 slider.isPressed()标志,您将获得要转换为毫秒的新值。当用户向左或向右滑动控件时,dur 变量用于移动 mediaPlayer 以在视频中寻找位置。每当滑块的值发生变化时,就会调用 ChangeListener 的 changed()方法。下面的代码负责根据用户移动滑块将搜索位置移动到视频中。

slider.valueProperty().addListener((ObservableValue<? extends Number> observable,
        Number oldValue, Number newValue) -> {
    if (slider.isPressed()) {
        long dur = newValue.intValue() * 1000;
        mediaPlayer.seek(new Duration(dur));
    }
});

接下来,您将实现一个拖放事件处理程序来处理放入应用窗口区域的媒体文件。在这里,该示例首先检查是否有以前的 mediaPlayer。如果有,则停止先前的 mediaPlayer 对象并执行清理:

        // stop previous media player and clean up
        if (mediaPlayer != null) {
           mediaPlayer.stop();
           mediaPlayer.currentTimeProperty().removeListener(progressListener);
           mediaPlayer.setOnPaused(null);
           mediaPlayer.setOnPlaying(null);
           mediaPlayer.setOnReady(null);
        }
       ...
       // play video when ready status
       mediaPlayer.setOnReady(() -> {
           progressSlider.setValue(1);
           progressSlider.setMax(mediaPlayer.getMedia().getDuration().toMillis() / 1000);
           mediaPlayer.play();
       });// setOnReady()

与音频播放器一样,您可以创建一个 Runnable 实例,在媒体播放器处于就绪状态时运行。您还会注意到 progressSlider 控件使用以秒为单位的值。

一旦媒体播放器对象处于就绪状态,就会创建一个 MediaView 实例来显示媒体。以下代码创建了一个 MediaView 对象,该对象将被放入场景图中以显示视频内容:

// Lazy init media viewer
if (mediaView == null) {
    mediaView = new MediaView();
    mediaView.setMediaPlayer(mediaPlayer);
    mediaView.setX(4);
    mediaView.setY(4);
    mediaView.setPreserveRatio(true);
    mediaView.setOpacity(.85);
    mediaView.setSmooth(true);

    mediaView.fitWidthProperty().bind(scene.widthProperty().subtract(220));
    mediaView.fitHeightProperty().bind(scene.heightProperty().subtract(30));

    // make media view as the second node on the scene.
    root.getChildren().add(1, mediaView);
}

// sometimes loading errors occur, print error when this happens
mediaView.setOnError((MediaErrorEvent event1) -> {
    event1.getMediaError().printStackTrace();
});

mediaView.setMediaPlayer(mediaPlayer);

event.setDropCompleted(success);
event.consume();
});

咻!您最终完成了场景的拖放事件处理程序。接下来几乎是媒体按钮控件的其余部分,类似于食谱 16-1 末尾的代码。唯一的区别是一个名为 paused 的 Boolean 类型的实例变量,它表示视频是否暂停。以下代码显示了 pauseButton 和 playButton 控制 mediaPlayer 对象并相应地设置暂停标志:

// pause media and swap button with play button
pauseButton.setOnMousePressed((MouseEvent me) -> {
    if (mediaPlayer != null) {
        buttonArea.getChildren().removeAll(pauseButton, playButton);
        buttonArea.getChildren().add(playButton);
        mediaPlayer.pause();
        paused = true;
    }
});

// play media and swap button with pause button
playButton.setOnMousePressed((MouseEvent me) -> {
    if (mediaPlayer != null) {
        buttonArea.getChildren().removeAll(pauseButton, playButton);
        buttonArea.getChildren().add(pauseButton);
        paused = false;
        mediaPlayer.play();
    }
});

这就是你如何创建一个视频媒体播放器。在下一个菜谱中,您将学习如何侦听媒体事件和调用操作。

16-3.控制媒体动作和事件

问题

您希望媒体播放器应用提供反馈来响应某些事件,例如当媒体播放器的暂停事件被触发时,在屏幕上显示文本“暂停”。

解决办法

您可以使用一个或多个媒体事件处理程序方法。表 16-3 中显示的是所有可能的媒体事件,它们被引发以允许开发者附加事件处理程序或可运行程序。

表 16-3。媒体事件
|

班级

|

设置方法

|

论方法属性方法

|

描述

|
| --- | --- | --- | --- |
| 媒体 | setOnError() | onErrorProperty() | 当错误发生时 |
| 媒体播放机 | setOnEndOfMedia() | onEndOfMediaProperty() | 已到达媒体播放的结尾 |
| 媒体播放机 | setOnError() | onErrorProperty() | 出现错误 |
| 媒体播放机 | setOnHalted() | onHaltedProperty() | 介质状态更改为暂停 |
| 媒体播放机 | 西顿马克 | onMarkerProperty() | 触发标记事件 |
| 媒体播放机 | setOnPaused() | onPausedProperty() | 发生暂停事件 |
| 媒体播放机 | setOnPlaying() | onPlayingProperty() | 媒体当前正在播放 |
| 媒体播放机 | setOnReady() | onReadyProperty() | 媒体播放器处于就绪状态 |
| 媒体播放机 | setOnRepeat() | onRepeatProperty() | 重复属性已设置 |
| 媒体播放机 | setOnStalled() | onStalledProperty() | 媒体播放器停止 |
| 媒体播放机 | setOnStopped() | onStoppedProperty() | 媒体播放器已停止 |
| MediaView(媒体检视) | setOnError() | onErrorProperty() | 媒体视图中出现错误 |

以下代码向用户显示“暂停”文本,其中“持续时间”的小数位数为毫秒。当用户点击暂停按钮时,该文本覆盖在视频顶部(参见图 16-4 )。

A323910_3_En_16_Fig4_HTML.jpg

图 16-4。暂停事件
   // when paused event display pause message
mediaPlayer.setOnPaused(() -> {
    pauseMessage.setText("Paused \nDuration: " +
       mediaPlayer.currentTimeProperty().getValue().toMillis());
    pauseMessage.setOpacity(.90);
});

它是如何工作的

事件驱动架构(EDA)是一种突出的架构模式,用于对异步传递消息的松散耦合组件和服务进行建模。JavaFX 团队将媒体 API 设计成事件驱动的,这个方法演示了如何实现它来响应媒体事件。

记住基于事件的编程,您将在调用函数时发现非阻塞或回调行为。在这个菜谱中,您将实现文本显示来响应 onPaused 事件,而不是将代码放入暂停按钮逻辑中。不是通过 EventHandler 将代码直接绑定到按钮,而是实现代码来响应媒体播放器被触发的 onPaused 事件。当响应媒体事件时,您将实现 java.lang.Runnables。

您会很高兴知道您一直在使用事件属性和实现 Runnables,尽管通常是以 lambda 表达式的形式。希望你在本章的所有食谱中都注意到了这一点。当媒体播放器处于就绪状态时,将调用可运行代码。为什么这是正确的?嗯,当媒体播放器加载完媒体时,onReady 属性会得到通知。这样,您可以确保调用 MediaPlayer 的 play()方法。我相信你会习惯事件风格的编程。以下代码片段演示了如何使用 lambda 表达式将 Runnable 实例设置到媒体播放器对象的 OnReady 属性中:

mediaPlayer.setOnReady(() -> {
   mediaPlayer.play();
});

为了让您看到 lambda 编程风格与旧风格之间的区别,下面是在没有使用 lambda 表达式的情况下实现的相同代码:

mediaPlayer.setOnReady(new Runnable() {
    @Override
    public void run() {
        mediaPlayer.play();
    }
});

看看你用 lambdas 去掉了多少行代码?您将采取与 onReady 属性类似的步骤。一旦暂停事件被触发,将调用 run()方法向用户显示一条消息,该消息包含一个带有单词 Paused 的文本节点和一个显示视频时间(毫秒)的持续时间。一旦文本显示出来,你可能想写下持续时间作为标记(你将在食谱 16-4 中学习)。下面的代码片段显示了一个附加的 Runnable 实例,该实例负责在视频中暂停时显示暂停的消息和持续时间(以毫秒为单位):

// when paused event display pause message
mediaPlayer.setOnPaused(() -> {
pauseMessage.setText("Paused \nDuration: " +
       mediaPlayer.currentTimeProperty().getValue().toMillis());
pauseMessage.setOpacity(.90);
});

16-4.标记视频中的位置

问题

您希望在媒体播放器应用中播放视频时提供隐藏式字幕文本。

解决办法

首先应用配方 16-3 中的溶液。通过从之前的配方中获取标记的持续时间(以毫秒为单位),您将在视频中的点创建媒体标记事件。对于每个媒体标记,您将关联将显示为隐藏字幕的文本。当标记经过时,文本将显示在右上角。

以下代码片段演示了在 Scene 对象的 onDragDropped 事件属性中处理的媒体标记事件:

... // inside the start() method

final VBox messageArea = createClosedCaptionArea(scene);
root.getChildren().add(messageArea);

// Dropping over surface
scene.setOnDragDropped((DragEvent event) -> {
    Dragboard db = event.getDragboard();
    boolean success = false;
    URI resourceUrlOrFile = null;

    // dragged from web browser address line?
    if (db.hasContent(DataFormat.URL)) {
        try {
            resourceUrlOrFile = new URI(db.getUrl().toString());
        } catch (URISyntaxException ex) {
            ex.printStackTrace();
        }
    } else if (db.hasFiles()) {
        // dragged from the file system
        String filePath = null;
        for (File file:db.getFiles()) {
            filePath = file.getAbsolutePath();
        }
        resourceUrlOrFile = new File(filePath).toURI();
        success = true;
    }
    // load media
    Media media = new Media(resourceUrlOrFile.toString());

    // stop previous media player and clean up
    if (mediaPlayer != null) {
        mediaPlayer.stop();
        mediaPlayer.currentTimeProperty().removeListener(progressListener);
        mediaPlayer.setOnPaused(null);
        mediaPlayer.setOnPlaying(null);
        mediaPlayer.setOnReady(null);
    }

    // create a new media player
    mediaPlayer = new MediaPlayer(media);

    // as the media is playing move the slider for progress
    mediaPlayer.currentTimeProperty().addListener(progressListener);

    // when paused event display pause message
    mediaPlayer.setOnPaused(() -> {
        pauseMessage.setOpacity(.90);
    });

    // when playing make pause text invisible
    mediaPlayer.setOnPlaying(() -> {
        pauseMessage.setOpacity(0);
    });

    // play video when ready status
    mediaPlayer.setOnReady(() -> {
        progressSlider.setValue(1);
        progressSlider.setMax(mediaPlayer.getMedia().getDuration().toMillis()/1000);
        mediaPlayer.play();
    });

    // Lazy init media viewer
    if (mediaView == null) {
        mediaView = new MediaView(mediaPlayer);
        mediaView.setX(4);
        mediaView.setY(4);
        mediaView.setPreserveRatio(true);
        mediaView.setOpacity(.85);
        mediaView.setSmooth(true);

        mediaView.fitWidthProperty().bind(scene.widthProperty().subtract(messageArea.widthProperty().add(70)));
        mediaView.fitHeightProperty().bind(scene.heightProperty().subtract(30));

        // make media view as the second node on the scene.
        root.getChildren().add(1, mediaView);
    }

    // sometimes loading errors occur
    mediaView.setOnError((MediaErrorEvent event1) -> {
        event1.getMediaError().printStackTrace();
    });

    mediaView.setMediaPlayer(mediaPlayer);

    media.getMarkers().put("First marker", Duration.millis(10000));
    media.getMarkers().put("Second marker", Duration.millis(20000));
    media.getMarkers().put("Last one...", Duration.millis(30000));

    // display closed caption
    mediaPlayer.setOnMarker((MediaMarkerEvent event1) -> {
        closedCaption.setText(event1.getMarker().getKey());
    });

    event.setDropCompleted(success);
    event.consume();
}); // end of setOnDragDropped

以下代码显示了一个工厂方法,该方法返回一个区域,该区域将包含显示在视频右侧的隐藏字幕:

private VBox createClosedCaptionArea(final Scene scene) {
    // create message area
    final VBox messageArea = new VBox(3);
    messageArea.setTranslateY(30);
    messageArea.translateXProperty().bind(scene.widthProperty().subtract(152) );
    messageArea.setTranslateY(20);
    closedCaption = new Text();
    closedCaption.setStroke(Color.WHITE);
    closedCaption.setFill(Color.YELLOW);
    closedCaption.setFont(new Font(15));

    messageArea.getChildren().add(closedCaption);
    return messageArea;
}

图 16-5 描绘了显示隐藏字幕文本的视频媒体播放器。

A323910_3_En_16_Fig5_HTML.jpg

图 16-5。隐藏字幕文本

它是如何工作的

媒体 API 有许多事件属性,开发人员可以将 EventHandlers 或 Runnables 实例附加到这些属性上,以便它们可以在事件被触发时做出响应。这个配方主要关注 OnMarker 事件属性。Marker 属性负责接收标记事件(MediaMarkerEvent)。

让我们从给媒体对象添加标记开始。它包含一个返回 Java FX . collections . observable map的 getMarkers()方法。使用可观察映射,您可以添加表示每个标记的键/值对。添加键应该是唯一的标识符,值是 Duration 的一个实例。为简单起见,此示例使用隐藏式字幕文本作为每个媒体标记的关键字。标记持续时间是指用户在配方 16-3 中确定的视频点按下暂停按钮时记录下来的时间。请注意,这不是用于生产质量代码的推荐方法。您可能希望使用平行贴图来代替。

添加标记后,您将使用 setOnMarker()方法在 MediaPlayer 对象的 OnMarker 属性中设置一个 EventHandler。接下来,通过 lambda 表达式实现一个 EventHandler 来处理引发的 MediaMarkerEvents。一旦接收到一个事件,您就获得了表示隐藏字幕中要使用的文本的键。实例变量 closed caption(Java FX . scene . text . text 节点)将通过使用与标记相关联的键或字符串调用 setText()方法来简单地显示。

这就是媒体标记。这说明了如何在视频中轻松地协调特效、动画等。

16-5.同步动画和媒体

问题

您希望在媒体显示应用中加入动画效果,例如在视频播放完毕后滚动文本“结束”。

解决办法

只需将配方 16-3 与配方 16-2 一起使用,即可获得所需的结果。配方 16-3 显示了如何响应媒体事件,配方 16-2 演示了如何使用翻译过渡来激活文本。

以下代码演示了触发媒体事件结束时的附加操作:

mediaPlayer.setOnEndOfMedia(() -> {
    closedCaption.setText("");
    animateTheEnd.getNode().setOpacity(.90);
    animateTheEnd.playFromStart();
    });

以下方法创建包含字符串“The End”的文本节点的 translateTransition,该字符串在触发媒体结束事件后出现:

    public TranslateTransition createTheEnd(Scene scene) {
    Text theEnd = new Text("The End");
    theEnd.setFont(new Font(40));
    theEnd.setStrokeWidth(3);
    theEnd.setFill(Color.WHITE);
    theEnd.setStroke(Color.WHITE);
    theEnd.setX(75);

    TranslateTransition scrollUp = new TranslateTransition();
    scrollUp.setNode(theEnd);
    scrollUp.setDuration(Duration.seconds(1));
    scrollUp.setInterpolator(Interpolator.EASE_IN);
    scrollUp.setFromY(scene.getHeight() + 40);
    scrollUp.setToY(scene.getHeight()/2);

    return scrollUp;
}

图 16-6 描绘了 OnEndOfMedia 事件被触发后“结束”文本节点的滚动。

A323910_3_En_16_Fig6_HTML.jpg

图 16-6。制作“结束”的动画

它是如何工作的

这个食谱展示了如何将事件与动画效果同步。在代码示例中,当视频到达结尾时,OnEndOfMedia 属性事件会启动一个 Runnable 实例。实例启动后,通过向上滚动包含字符串“The End”的文本节点来执行 TranslateTransition 动画。

让我们来看看与 MediaPlayer 对象相关联的 setOnEndOfMedia()方法。就像在菜谱 16-3 中一样,你只需通过传入一个实现 Runnable 的 lambda 表达式来调用 setOnEndOfMedia()方法,该表达式包含调用动画的代码。如果你不知道动画是如何工作的,参考食谱 16-2。一旦事件发生,您将看到文本向上滚动。以下代码片段来自 scene.setOnDragDropped()方法内部:

mediaPlayer.setOnEndOfMedia(() -> {
    closedCaption.setText("");
    animateTheEnd.getNode().setOpacity(.90);
    animateTheEnd.playFromStart();
    });

出于篇幅的考虑,我相信您知道代码块会放在哪里。如果没有,参考配方 16-3,其中你会注意到其他 OnXXX 属性方法。要查看完整的代码清单并下载源代码,请访问该书的网站。

要制作“结尾”的动画,需要创建一个方便的 createTheEnd()方法来创建一个文本节点的实例,并将 TranslateTransition 对象返回给调用者。返回的 TranslateTransition 执行以下操作:在播放视频之前等待一秒钟。接下来是您使用插值器的插值器。EASE_IN 通过在句号前缓入来移动文本节点。最后,设置节点的 Y 属性,从查看区域的底部移动到中心。

下面的代码创建了一个向上滚动节点的动画:

TranslateTransition scrollUp = new TranslateTransition();
scrollUp.setNode(theEnd);
scrollUp.setDuration(Duration.seconds(1));
scrollUp.setInterpolator(Interpolator.EASE_IN);
scrollUp.setFromY(scene.getHeight() + 40);
scrollUp.setToY(scene.getHeight()/2);

摘要

JavaFX 从一开始就是开发基于媒体的应用的场所。JavaFX Media API 使开发人员能够轻松地向任何应用添加媒体和基于媒体的控件。在 JavaFX 的早期版本中,视频和音频类型更加有限。Java 8 增加了对不同媒体类型的支持,还增加了通过 lambda 表达式实现媒体控制的能力。

本章简要概述了一些 JavaFX 媒体 API 功能。然而,我们甚至还没有触及可能性的表面。有关 JavaFX 媒体 API 的更多信息,请参见位于docs . Oracle . com/javase/8/Java FX/API/Java FX/scene/Media/package-summary . html的在线文档。

十七、使用 JavaServer Faces 的 Java Web 应用

Java 开发不仅仅是在桌面上。数以千计的企业应用是使用 Java 企业版(Java EE)编写的,这使得开发复杂、健壮和安全的应用成为可能。开发 Java EE 应用的最主流和最成熟的框架是 JavaServer Faces (JSF)。JDK 9 可以与一些 Java EE 应用服务器(如 GlassFish)一起使用,以支持 Java 9 特性的使用。虽然 Java EE 和 JSF 太大了,无法在一章中涵盖,但这将让您对使用 Java 9 和 Java EE 进行 web 开发的世界有所了解。

在这一章中,我将介绍 JSF 框架的基础知识,从开发一个基本的应用到创建一个复杂的前端。在整个过程中,我将介绍一些重要的信息,比如如何正确地确定控制器类的范围,以及如何生成 web 应用模板。最终,您将能够开始开发 Java web 应用,或者维护现有的 JSF 项目。

由于 web 应用开发包含许多相互关联的过程,因此建议利用 NetBeans 之类的集成开发环境来更轻松地组织 web 项目。在本章中,我将使用 NetBeans IDE 8.2 演示配方的解决方案。但是,您可以使用任意数量的 Java IDEs 将这些基本概念应用到项目中。

注意

这本书是使用 GlassFish 5 应用服务器和 JDK 9 的早期访问版本编写的。要配置服务器使用 JDK 9,修改 GlassFish<>/config/asenv . conf 文件并添加 AS_JAVA 属性,指向 JDK 9 的安装。接下来,修改< >/bin/asadmin 文件,使最后一行如下所示:

exec " $ JAVA "-add-modules JAVA . annotations . common-jar " $ AS _ INSTALL _ LIB/client/appserver-CLI . jar " " $ @ "

17-1.创建和配置 Web 项目

问题

您希望创建并配置一个简单的 Java web 应用项目,该项目将利用 JSF web 框架。

解决办法

有许多不同的项目格式可用于创建 web 应用。其中最灵活的是 Maven web 应用格式。随着时间的推移,Apache Maven 构建系统使组织构建和扩展应用的功能变得容易,因为它包含一个健壮的依赖管理系统。在该解决方案中,利用 NetBeans IDE 生成一个 Maven Web 应用项目,然后配置该项目以开发 JSF 应用。

首先,打开 NetBeans IDE 并选择“文件”、“新建项目”,然后在“新建项目”窗口中,选择“Maven”类别和“Web 应用”项目(图 17-1 ),然后单击“下一步”

A323910_3_En_17_Fig1_HTML.jpg

图 17-1。NetBeans Maven Web 应用项目

将应用命名为“HelloJsf”,并将其放入硬盘上的一个目录中。将“包名”更改为 org.java9recipes,并保留所有其他默认值(图 17-2 )。

A323910_3_En_17_Fig2_HTML.jpg

图 17-2。新的 Java Web 应用配置

接下来,选择应用将部署到的服务器,以及 Java EE 版本。在这种情况下,我将利用 Payara 5 服务器(GlassFish 也足够了)和 Java EE 7(图 17-3 )。

A323910_3_En_17_Fig3_HTML.jpg

图 17-3。选择服务器和 Java EE 版本

创建项目后,右键单击项目并选择“Properties ”,为 JSF 配置它并分配一个 Java 平台。在属性菜单中,选择“框架”类别,然后选择“添加”并选择 JSF。接下来,在同一窗口中点击“组件”选项卡,并选择“PrimeFaces”(图 17-4 )。

A323910_3_En_17_Fig4_HTML.jpg

图 17-4。配置项目属性

单击“OK”保存项目属性,项目现在可以使用 JSF 作为框架,连同 PrimeFaces UI 库一起构建了。

它是如何工作的

web 应用的开发需要编排许多不同的文件。虽然可以在不使用 IDE 的情况下开发 Java EE web 应用,但是使用开发环境几乎是一件小事。在这个方法中,NetBeans IDE 用于配置基于 Maven 的 web 应用。Maven 是一个类似于 Apache Ant 的构建系统,它对于应用项目的组织非常有用。Maven 不一定比 Ant 好,但是更容易上手使用。Ant 和 Maven 都是构建系统;然而,Maven 使用约定胜于配置,因此它假设了许多默认配置,以便用户可以非常容易地使用。另一方面,Ant 需要在使用之前配置并编写一个构建脚本。Maven 的一个关键组件是它使依赖关系管理变得非常容易。它已经成为最流行的项目格式之一,在 NetBeans 中开发 Maven 项目可以创建可移植的项目。

在项目创建向导中,必须填写许多字段,尽管许多缺省值可以保留。最重要的是,为应用设置适当的包命名约定,并选择服务器和 Java EE 版本。

注意

使用向导时完成的设置可以在项目创建后通过进入项目属性进行更改。

一旦初始向导完成,就会生成一个基本的 Maven web 项目。此时,通过更改项目属性,可以将项目配置为利用 web 框架、不同版本的 JDK 等。右键单击 NetBeans 项目以进入项目属性屏幕,并利用类别选择来查看或更改与选定类别相关的属性。在这种情况下,选择“框架”类别将允许您添加一个 web 框架,如 JSF。当框架添加到项目中时,框架的所有管道和配置都已完成。另外,在选择 JSF 时,选择框架属性上的“Components”选项卡,并添加将要使用的任何其他 JSF 库。在这种情况下,添加“PrimeFaces ”,因为本章开发的应用将利用 PrimeFaces 组件库。

一旦配置好框架,请确保在属性对话框中选择“源代码”类别,并选择将用于应用编码的 JDK 版本的“源代码/二进制格式”。在这种情况下,选择 1.8,因为在撰写本文时,Java 9 还没有被认证为可以在应用服务器上运行。接下来,在属性对话框中选择“Build”->“Compile”类别,并确保“Java Platform”选项与“Source/Binary Format”类别中选择的选项一致。

完成这些选择后,配置就完成了。在项目属性中选择“确定”。该项目将被修改以包括新的视图(index.xhtml 和 welcomePrimefaces.xhtml)(图 17-5 )。对于 JSF 配置,web.xml 部署描述符也将改变。欢迎文件现在将指向 index.xhtml,JSF 框架的关键组件 FacesServlet 将被配置。

A323910_3_En_17_Fig5_HTML.jpg

图 17-5。完全为 JSF 配置的 Maven Web 项目

JSF 应用的 web.xml 配置通常如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>/faces/*</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

此时,右键单击 NetBeans 项目并选择“运行”这将导致应用被编译并部署到在项目属性中或在项目创建时选择的应用服务器上(图 17-6 和 17-7 )。

A323910_3_En_17_Fig6_HTML.jpg

图 17-6。部署的 HelloJsf 应用

A323910_3_En_17_Fig7_HTML.jpg

图 17-7。选择“欢迎使用 PrimeFaces”链接

这就是在 NetBeans 中创建和配置 JSF 项目的过程。在下一个菜谱中,我将深入研究 JSF 的世界,因为 HelloJsf 应用被修改以添加一些功能。

17-2.开发 JSF 应用

问题

您已经创建了一个配置了 JSF 的 Maven web 项目,并且希望向应用添加功能。

解决办法

构建应用,使其包含一个 HTML 表单,其中包含许多要填充的字段。提交表单时,表单将调用一个控制器方法。

首先,创建一个 Java 类,它将被用作保存表单中提交的数据的容器。在名为 org . Java 9 recipes . hello JSF . model 的包中创建一个新的 Java 类,并将其命名为 User。在该类中,暂时创建三个 String 类型的私有字段:firstName、lastName 和 email。接下来,右键单击文件并从上下文菜单中选择“Refactor->Encapsulate Fields ”,为这些字段生成访问器方法(getters 和 setters)。这将打开“封装字段”对话框,在该对话框中,您应该选择所有用于创建访问器方法的字段,然后单击“重构”(图 17-8 )。

A323910_3_En_17_Fig8_HTML.jpg

图 17-8。封装字段

接下来,创建上下文和依赖注入(CDI)托管 bean。右键单击项目的“Source Packages”节点,创建一个名为 org.java9recipes.hellojsf.jsf 的新包,它将用于打包应用的所有托管 bean 控制器类。接下来,在名为 HelloJsfController 的新包中创建一个新的 Java 类,并使该类实现 java.io.Serializable,以便它能够钝化。用@ViewScoped 对该类进行注释,以表明该 bean 将在视图作用域中进行管理(关于作用域的更多信息,请参见配方 17-6)。此外,用@Named 对该类进行注释,这使得控制器类可注入,并且还允许在 JSF 视图中从表达式语言引用该类。接下来,创建一个 user 类型的私有字段,将字段命名为 User,并封装字段以生成访问器方法。在生成的 getUser()方法中,执行检查以查看用户字段是否为空,如果是,则实例化一个新用户。此时,该类应该如下所示:

package org.java9recipes.hellojsf.jsf;

import javax.faces.view.ViewScoped;
import javax.inject.Named;
import org.java9recipes.hellojsf.model.User;

@Named
@ViewScoped
public class HelloJsfController implements java.io.Serializable {

    private User user;

    public User getUser() {
          if(user == null){
            user = new User();
        }
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

}

最后,创建一个具有 void 返回类型的公共方法,并将其命名为 createUser()。当有人单击表单上的提交按钮时,将调用该方法。在该方法中,只需在屏幕上打印一条消息,表明用户已经成功创建。为此,获取当前 FacesContext 实例的句柄,该实例属于当前会话。一旦获得,通过传递一个 null 作为第一个参数向其添加一个新的 FacesMessage,因为该消息不会被分配给任何单个组件,并传递该消息作为第二个参数。最后,将用户对象设置为 null,以便创建新的用户对象。

注意

FacesContext 包含关于 JSF 请求的状态信息。FacesContext 在 JSF 请求处理生命周期的不同阶段都会更新。

一旦完成,该方法应该如下所示。

public void createUser(){
    FacesContext context = FacesContext.getCurrentInstance();
    context.addMessage(null, new FacesMessage("Successfully Added User: " +
                        user.getFirstName() + " " + user.getLastName()));
    user = null;
}

接下来,是时候创建视图了。在这种情况下,请在 NetBeans IDE 中打开 index.xhtml 视图文件,并添加构成表单的 html 标记和 JSF 组件。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html 
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:p="http://primefaces.org/ui">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        Hello from Facelets
        <br />
        <h:link outcome="welcomePrimefaces" value="Primefaces welcome page" />
        <br/>
        <h:form>
            <p:messages id="messages"/>
            <br/>
            <p:outputLabel for="firstName" value="First: "/>
            <p:inputText id="firstName" value="#{helloJsfController.user.firstName}"/>
            <br/>
            <p:outputLabel for="lastName" value="Last: " />
            <p:inputText id="lastName" value="#{helloJsfController.user.lastName}"/>
            <br/>
            <p:outputLabel for="email" value="Email: " />
            <p:inputText id="email" value="#{helloJsfController.user.email}"/>
            <br/><br/>
            <p:commandButton id="submitUser" value="Submit" ajax="false"
                             action="#{helloJsfController.createUser()}"/>

        </h:form>
    </h:body>
</html>

一旦生成了视图并创建了 CDI 控制器,就可以通过右键单击项目并选择“Run”来构建和运行应用该屏幕将类似于图 17-9 中的屏幕。

A323910_3_En_17_Fig9_HTML.jpg

图 17-9。JSF 形式

它是如何工作的

JSF 是 Sun Microsystems 在 2004 年开发的,旨在帮助简化 web 应用开发,并使 web 应用更易于管理/支持。它是 JavaServer Pages (JSP)框架的发展,增加了更有组织的开发生命周期和更容易利用现代 web 技术的能力。JSF 使用 XHTML 格式的 XML 文件构建视图,使用 Java 类构建应用逻辑,这使得它能够遵循 MVC 架构。JSF 应用中的每个请求都由 FacesServlet 处理。FacesServlet 负责构建组件树、处理事件、确定导航和呈现响应。JSF 现在已经成为一个成熟的网络框架,与之前的版本相比有很多优势。还有大量的组件和函数库可用于扩展 JSF 应用。

该框架非常强大,包括与 Ajax 和 HTML5 等技术的轻松集成,使得开发动态内容毫不费力。JSF 可以很好地处理数据库,使用 JDBC、企业 Java Bean (EJB)或 RESTful 技术来处理后端。JavaBeans 被称为 JSF 托管 Beans,用于应用逻辑并支持每个视图中的动态内容。根据所使用的范围(配方 17-6),它们可以坚持不同的寿命。视图可以调用 beans 中的方法来执行数据操作和表单处理等操作。属性也可以在 beans 中声明,在视图中公开,并使用标准的表达式语言进行计算,这提供了一种与服务器之间传递值的便捷方式。JSF 允许开发人员使用预先存在的验证和转换标签定制他们的应用,这些标签可以用在带有视图的组件上。构建定制验证器和定制组件也很容易,它们可以应用于视图中的组件。简而言之,JSFs 的成熟使得使用该框架开发任何 web 应用都变得很容易。

在这个解决方案中,创建了一个名为 HelloJsf 的小应用。application 视图包含一个用于提交几个数据字段的简单 HTML 表单、一个用于向后端提交表单的按钮和一个用于显示响应的消息组件。名为 UserController 的控制器类是 ViewScoped 的,这意味着该类中的对象范围将在视图的生命周期内保留。一旦用户导航到另一个视图或关闭窗口,对象就会被销毁。一个名为 User 的对象用作在应用中传递用户数据的容器,用户在控制器类中声明,并通过访问器方法供视图使用。

private User user;

/**
 * @return the user
 */
public User getUser() {
    if(user == null){
        user = new User();
    }
    return user;
}

/**
 * @param user the user to set
 */
public void setUser(User user) {
    this.user = user;
}

CDI 控制器类包含 JSF 应用视图的业务逻辑。在该解决方案中,名为 HelloJsfController 的类管理 HelloJsf 应用的处理和数据。控制器的代码可以在配方 17-4 中看到。控制器负责向 JSF 视图显示字段和操作方法,以便可以将数据直接提交到字段中并进行相应的处理。控制器还有助于与终端用户的通信,因为可以创建消息来清楚地指示处理是否成功或者是否出现了问题,并且可以使消息对视图可用。

应用的视图是一个 XHTML 文件 index.xhtml,它包含一个通过 JSF

标记的 HTML 表单。在视图的顶部,导入了所需的名称空间,以便可以利用 PrimeFaces 和 JSF HTML 组件。该表单由许多 HTML 元素和 JSF 组件组成。PrimeFaces 组件必须以“p”为前缀,因为 PrimeFaces 名称空间被分配给该字母。每个 JSF 组件都包含许多属性,可用于设置值和配置组件的行为和功能。消息组件用于显示通过 FacesContext 提供的消息。p:outputLabel 组件呈现为 HTML 标签,p:inputText 组件呈现为 Text 类型的 HTML 输入元素。p:inputText 组件的 value 属性包含 JSF 表达式语言,引用 HelloJsfController 用户对象字段。最后,p:commandButton 组件呈现一个 HTML 按钮(类型为“submit”的输入元素)来提交表单。commandButton 的 action 属性也利用 JSF 表达式语言来调用名为 createUser()的控制器操作方法。ajax="false "属性表明 ajax 不应该用来异步处理表单值,而是应该提交和刷新表单。

这个菜谱包含了很多信息,但是它展示了用托管控制器类开发一个简单的 JSF 视图是多么容易。在实际应用中,数据可能存储在 RDBMS 中,如 Oracle 等。下一个诀窍是如何添加一个数据库并将其绑定到应用来存储和检索用户对象。

17-3.开发数据模型

问题

您希望将来自 Java EE 应用的数据存储在关系数据库中。

解决办法

将应用中的数据绑定到 Java 对象,以便可以使用这些对象来存储和检索数据库中的数据。在大多数情况下,Java 持久性 API (JPA)是处理 Java 对象形式的数据的合适选择。在前面的配方中,开发了一个 JSF 应用,用于将用户对象提交给 CDI 控制器。在这个配方中,数据将被绑定到一个实体类,然后使用 JPA 从关系数据存储中存储/检索。

出于这个菜谱的目的,将使用 Apache derby 数据库。首先,创建一个数据库表来存储用户对象。以下 SQL 可用于生成表,该表包括标识为 ID 的主键字段。

CREATE TABLE HELLO_USER (
ID                          NUMERIC PRIMARY KEY,
FIRST_NAME                  VARCHAR(100),
LAST_NAME                   VARCHAR(50),
EMAIL                       VARCHAR(150));

一旦创建了数据库表,就生成一个相应的实体类。对于此解决方案,将使用 NetBeans IDE 来自动创建该类。为此,右键单击 HelloJsf 项目的“Source Packages”节点,并创建一个名为 org . Java 9 recipes . hello JSF . Entity 的包。出现“从数据库新建实体类”对话框后,为 Apache Derby 数据库选择或创建一个 JDBC 数据源。选中后,从可用表列表中选择用户表,并将其添加到“选定的表”列表中,然后选择“下一步”在随后的对话框屏幕上,接受所有默认设置,点击“完成”并创建实体类(图 17-10 )。

A323910_3_En_17_Fig10_HTML.jpg

图 17-10。在 NetBeans IDE 中从数据库创建实体类

一旦创建了实体类,开发一个 EJB 或 JAX-RS RESTful web 服务类来处理相应的实体。在此解决方案中,将使用 NetBeans IDE 开发一个 EJB,首先在名为 org . Java 9 recipes . hello JSF . session 的项目中创建另一个新包。此包将用于保存会话 beans 或 EJB。接下来,右键单击新创建的包,并从上下文菜单中选择“实体类的会话 Beans”。这将打开允许选择实体类的对话框,以便 NetBeans IDE 可以自动创建相应的会话 beans(图 17-11 )。

A323910_3_En_17_Fig11_HTML.jpg

图 17-11。选择实体类以生成会话 beans

选择后,选择“下一步”,最后选择“完成”以创建 EJB。这样做之后,NetBeans IDE 将生成一个名为 AbstractFacade 的抽象类,它将由生成的任何实体类进行扩展。NetBeans IDE 还将生成会话 bean hellojsfacade。一旦生成了这些类,应用的模型就完成了,控制器将能够成功地处理数据。

它是如何工作的

企业应用的模型是最重要的组件之一,因为数据是企业的核心。要为 Java EE 应用生成模型,必须有一个数据存储,通常是一个 RDBMS,并且必须编码一个对象关系映射策略,以代码格式表示数据库。在这个解决方案中,模型由三个类组成:实体类、包含标准对象关系映射方法的抽象类和扩展抽象类的 EJB。

实体类本质上是一个普通的旧 Java 对象(POJO ),它将数据库表表示为一个 Java 对象。实体类为数据库表的每一列都声明了一个字段,并为每个字段定义了访问器方法。注释使实体类像魔术一样工作,通过几个简单的注释执行绑定类的任务,随后将字段绑定到数据库表及其列。@Entity 注释告诉编译器这是一个实体类。表 17-1 列出了一些常见的实体类注释。

表 17-1。公共实体类注释
|

注释

|

描述

|
| --- | --- |
| @实体 | 将类标记为实体类。 |
| @表格 | 将实体类映射到数据库表。 |
| @Id | 降级实体类的主键字段。 |
| @XmlRootElement | 将类映射到 XML 元素。 |
| @ NamedQueries | 将名称映射到预定义查询的@NamedQuery 元素列表。 |
| @可嵌入 | 降级嵌入的类。 |

通过用@Table 对实体类进行注释,并将数据库表的名称指定为属性,将实体类映射到命名的数据库表。为了方便起见,NetBeans IDE 还向实体类添加了几个注释,分别是@XmlRootElement 和@NamedQueries。@XmlRootElement 注释将 XML 根元素与类相关联,从而使实体类可用于基于 XML 的 API,如 JAX-RS 和 JAXB。@NamedQueries 注释为实体提供了许多命名查询(每个字段一个),使得按名称查询实体类变得容易,而不是每次需要查询时都编写 JPQL。实体类也总是包含一个主键,它通过@Id 注释来表示,数据库表的每一列都用@Column 映射到类字段。Bean 验证也可以添加到实体类的字段中,为添加到相关实体类字段中的任何输入或内容提供验证。最后,一个实体类包含一个 equals()方法来帮助比较对象和实体,以及一个 toString()方法。HelloUser 的最终实体类应该如下所示:

@Entity
@Table(name = "HELLO_USER")
@XmlRootElement
@NamedQueries({
    @NamedQuery(name = "HelloUser.findAll", query = "SELECT h FROM HelloUser h"),
    @NamedQuery(name = "HelloUser.findById", query = "SELECT h FROM HelloUser h WHERE h.id = :id"),
    @NamedQuery(name = "HelloUser.findByFirstName", query = "SELECT h FROM HelloUser h WHERE h.firstName = :firstName"),
    @NamedQuery(name = "HelloUser.findByLastName", query = "SELECT h FROM HelloUser h WHERE h.lastName = :lastName"),
    @NamedQuery(name = "HelloUser.findByEmail", query = "SELECT h FROM HelloUser h WHERE h.email = :email")})
public class HelloUser implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @Basic(optional = false)
    @NotNull
    @Column(name = "ID")
    private Integer id;
    @Size(max = 100)
    @Column(name = "FIRST_NAME")
    private String firstName;
    @Size(max = 50)
    @Column(name = "LAST_NAME")
    private String lastName;
    // @Pattern(regexp="[a-z0-9!#$%&'*+/=?^_`{|}∼-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}∼-]+)*@(?:a-z0-9?\\.)+a-z0-9?", message="Invalid email")//if the field contains e-mail address consider using this annotation to enforce field validation
    @Size(max = 150)
    @Column(name = "EMAIL")
    private String email;

    public HelloUser() {
    }

    public HelloUser(Integer id) {
        this.id = id;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof HelloUser)) {
            return false;
        }
        HelloUser other = (HelloUser) object;
        if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "org.java9recipes.hellojsf.entity.HelloUser[ id=" + id + " ]";
    }

}

一旦生成了一个实体类,就可以生成一个会话 bean 来促进实体类的工作。会话 bean(又名 EJB)声明一个 PersistenceContext,它提供与底层数据存储的通信。然后它调用 PersistenceContext 来执行任意数量的 JPA 任务,比如通过实体类数据创建、更新或删除数据库中的记录。NetBeans IDE 生成 AbstractFacade 抽象类,该类由项目的所有实体类扩展。这个类本质上包含了允许对实体进行基本操作的方法:create()、findAll()、edit()和 remove(),使开发人员能够自动访问这些方法,而无需为每个实体类重新编码。这给开发人员留下了一个没有任何编码的全功能会话 bean。如果需要创建针对实体的附加查询或工作,开发人员可以相应地修改会话 bean 的内容,在本例中是 HelloJsfFacade。

EJB 必须用@Stateful 或@Stateless 注释,以指定该类是有状态会话 bean 还是无状态会话 bean。有状态会话 bean 可以绑定到单个用户会话,允许在整个用户会话中管理状态。无状态更常用于在应用的所有用户会话中共享会话 bean。名为 HelloJsfFacade 的简单无状态会话 bean 如下所示:

@Stateless
public class HelloUserFacade extends AbstractFacade<HelloUser> {

    @PersistenceContext(unitName = "org.java9recipes_HelloJsf_war_1.0-SNAPSHOTPU")
    private EntityManager em;

    @Override
    protected EntityManager getEntityManager() {
        return em;
    }

    public HelloUserFacade() {
        super(HelloUser.class);
    }

}

本菜谱中讨论的类和代码构成了应用的模型。总之,该模型将应用绑定到底层数据存储,从而使得使用 Java 对象创建、移除、更新和删除数据成为可能,而不是通过 SQL 直接使用数据库。关于开发实体类的更多信息,请参见在线 Java EE 教程:docs.oracle.com/javaee/7/tutorial/

17-4.编写视图控制器

问题

您已经开发了一个 JSF 视图,其中包含绑定字段和一个表单,您需要创建业务逻辑来处理表单并简化会话 bean 的工作。

解决办法

创建一个托管 bean 控制器类(CDI bean ),该类可用于将操作和字段绑定到 JSF 视图,并简化需要在 EJB 会话 bean 中执行的工作。在这个解决方案中,HelloJsfController 类(如下所示)被用作 CDI 控制器。

@Named
@ViewScoped
public class HelloJsfController implements java.io.Serializable {

    private User user;

    /**
     * @return the user
     */
    public User getUser() {
        if(user == null){
            user = new User();
        }
        return user;
    }

    /**
     * @param user the user to set
     */
    public void setUser(User user) {
        this.user = user;
    }

    public void createUser(){
        FacesContext context = FacesContext.getCurrentInstance();
        context.addMessage(null, new FacesMessage("Successfully Added User: " +
                            user.getFirstName() + " " + user.getLastName()));
        user = null;
    }

}

在前面的菜谱中,数据模型被添加到 HelloJsf 应用中。接下来,需要修改控制器类和视图以利用数据模型。要修改控制器,只需添加一个新的 HelloUser 类型的私有字段,并为其生成访问器方法。在 getHelloUser 方法中,首先检查字段是否为 null,如果是,实例化一个新的实例。

...
private HelloUser helloUser;
...
public HelloUser getHelloUser() {
        if(helloUser == null){
            helloUser = new HelloUser();
        }
        return helloUser;
}

public void setHelloUser(HelloUser helloUser) {
        this.helloUser = helloUser;
}
...

接下来,将 EJB 注入控制器类,这样就可以持久保存一个新的 HelloUser。为此,注入一个新的 HelloUserFacade 类型的私有字段,如下所示:

@EJB
private HelloUserFacade helloUserFacade;

最后,创建一个名为 createAndPersistUser()的新操作方法,它通常与 createUser()方法做相同的事情。然而,这个新方法将通过调用 EJB 把一个更高级的对象持久化到数据库中。

public void createAndPersistUser(){
    FacesContext context = FacesContext.getCurrentInstance();
    helloUserFacade.create(helloUser);
    context.addMessage(null, new FacesMessage("Successfully Persisted User: " +
                        user.getFirstName() + " " + user.getLastName()));
    helloUser = null;
}

数据模型现在已经集成到控制器逻辑中。当用户单击视图中的按钮时,它应该调用控制器中的操作方法 createAndPersistUser()。表单中包含的字段也通过控制器处理,因为用户对象被注入并暴露给用户界面。

它是如何工作的

JSF 管理的 bean 控制器用于促进 Java EE 应用的视图和会话 bean 之间的工作。过去,受管 bean 控制器遵循一套不同的规则,因为 JSF 包含自己的一套用于开发受管 bean 的注释。在 Java EE 的最近版本中,特定于 JSF 的托管 bean 已经被淘汰,CDI beans 已经取而代之,从而允许更具内聚性和通用性的控制器类。

在该解决方案中,该类实现 java.io.Serializable,因为在会话突然结束的情况下,可能需要将它保存到磁盘上。这个类用@Named 进行了注释,使它可以通过 JSF 表达式语言进行注入和访问。该类还用指定的 CDI 作用域进行了注释,在本例中为@ViewScoped,以指示控制器的 CDI 作用域。有许多不同的范围,这些包括在配方 17-6 中。ViewScoped 意味着控制器状态将在视图的生存期内保存。用户对象在控制器中被声明为私有字段,并且可以通过访问器方法作为属性进行访问。最后,该类包含一个名为 createUser()的方法,该方法是公共的,它创建一个 FacesMessage 对象并将其放入当前的 FacesContext 中以在屏幕上显示。然后,用户对象被设置为 null。

控制器类的修改版本(包括数据模型)声明了 HelloUser 类型的实例字段。创建 HelloUser 字段的访问器方法,如果该字段为空,则在 getter 方法中创建一个新实例。HelloUserFacade 被注入到控制器中,以便它可以用来执行数据模型事务(也称为:数据库事务)。createAndPersistUser()方法调用 HelloUserFacade create()方法,传递一个 HelloUser 实例将对象持久化到数据库中。类似地,如果希望编辑 HelloUser 对象,可以调用 HelloUserFacade edit()方法。最后,如果希望删除用户,可以调用 remove()方法。

控制器类可以包含任意数量的动作方法和字段声明,但是,管理控制器的大小以使控制器不负责执行过多的工作是很重要的。如果一个控制器类包含太多的功能,例如,如果它用于支持多个视图,那么它会变得很麻烦,很难维护。要了解更多关于控制器类的 CDI 作用域,请参阅配方 17-6。

17-5.开发异步视图

问题

与旧式的提交和响应 web 应用不同,您希望生成一个现代化的 ui,它可以异步提交数据和发布响应,而无需刷新浏览器页面或重新呈现视图,从而提供更好的用户体验。

解决办法

将异步 JavaScript 和 XML 合并到您的应用中,以异步方式向服务器发送数据,并在不刷新的情况下呈现响应。为 JSF 应用创建基于 AJAX 的视图有很多种方法,这个方法将演示如何利用 PrimeFaces AJAX API。在这个解决方案中,将创建一个名为 helloAjax.xhtml 的新视图(图 17-12 ),它通常是利用 Ajax 提交表单的原始 index.xhtml 视图的副本。该视图还将异步更新 messages 组件,显示控制器类生成的消息。helloAjax.xhtml 中还添加了一个 dataTable 组件,它被异步更新以显示已经创建并保存到数据库中的用户列表。增强视图如下所示:

A323910_3_En_17_Fig12_HTML.jpg

图 17-12。异步形式
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html 
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:p="http://primefaces.org/ui">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        Hello from Facelets
        <br />
        <h:link outcome="welcomePrimefaces" value="Primefaces welcome page" />
        <br/>
        <h:form>
            <h:inputText id="firstNameType" value="#{helloJsfController.freeText}">
                <f:ajax execute="@this" event="keyup" listener="#{helloJsfController.displayText}"
                        render="messages"/>
            </h:inputText>
            <p:messages id="messages"/>
            <br/>
            <p:panelGrid columns="2" style="width: 100%">

                <p:outputLabel for="firstName" value="First: "/>
                <p:inputText id="firstName" value="#{helloJsfController.user.firstName}"/>

                <p:outputLabel for="lastName" value="Last: " />
                <p:inputText id="lastName" value="#{helloJsfController.user.lastName}"/>

                <p:outputLabel for="email" value="Email: " />
                <p:inputText id="email" value="#{helloJsfController.user.email}"/>
            </p:panelGrid>
            <br/>
            <p:commandButton id="submitUser" value="Submit"
                             action="#{helloJsfController.createUser()}"
                             update="messages, helloUsers"/>

            <br/>
            <p:dataTable id="helloUsers" var="user" value="#{helloJsfController.helloUserList}">
                <p:column headerText="First Name">
                    <h:outputText value="#{user.firstName}"/>
                </p:column>
                <p:column headerText="Last Name">
                    <h:outputText value="#{user.lastName}"/>
                </p:column>
                <p:column headerText="Email">
                    <h:outputText value="#{user.email}"/>
                </p:column>
            </p:dataTable>

        </h:form>
    </h:body>
</html>

它是如何工作的

将 AJAX 的原理应用于 JSF 视图是非常容易的。有几种不同的方法来应用 AJAX 功能,但是最简单的方法是利用一个复杂的用户界面框架,比如 PrimeFaces,它包含了内置的 AJAX 功能。事实上,许多 PrimeFaces 组件默认执行 ajax 提交,因此它们包含一个 AJAX 属性,可以设置为 false 以便以同步方式操作。

在这个配方的解决方案中,PrimeFaces commandButton 用于将表单内容异步发送到控制器类。一旦调用了 action 方法,数据就被持久化,并生成 FacesMessage,然后响应被发送回视图。当视图收到响应时,它异步更新 commandButton update 属性中列出的组件,即 messages 组件和 helloUsers dataTable 组件。

通过在组件的开始和结束标签之间嵌入标签,可以异步提交没有 PrimeFaces 的 JSF 组件的内容。f:ajax 标签使用 execute 属性来指示视图的哪个部分将被异步执行或提交,使用 onevent 属性来指示哪个 JavaScript 事件应该调用异步操作,使用 listener 属性来绑定操作方法,等等。例如,通过使用 f:ajax 标记,下面的 inputText 组件变成了异步的:

<h:inputText id="firstNameType" value="#{helloJsfController.freeText}">
   <f:ajax execute="@this" event="keyup" listener="#{helloJsfController.displayText}"
                        render="messages"/>
</h:inputText>

在该示例中,当 keyup 事件发生时,在 inputText 字段中键入的值被提交给 helloJsfController.freeText 属性。displayText()操作方法也被调用,该方法将 freeText 属性的内容放入 FacesMessage 中,如下所示。一旦动作被调用并且请求被发回,消息组件就会被更新,因为 f:ajax 的 render 属性指定了它的 id。

public void displayText(AjaxBehaviorEvent evt){
    FacesContext context = FacesContext.getCurrentInstance();
    System.out.println("test: " + freeText);
    context.addMessage(null, new FacesMessage(freeText));
}

有许多不同的技术可以用来异步更新 JSF 视图。在众多可用的 UI 库中,还有更多可用的异步组件。虽然这个解决方案演示了 PrimeFaces 以及 f:ajax 标签的使用,但是可以就这个主题写一些小书。JSF 是一个成熟的网络框架,提供了大量的工具来完成这项工作。选择最适合这种情况的方法,享受使用 AJAX 和限制暴露于底层 JavaScript 的便利。

17-6.应用正确的范围

问题

您正在开发一个 JSF 应用,并且您希望确保控制器被配置为根据功能和需求在正确的时间内保持在范围内。

解决办法

利用 CDI 作用域将所需的作用域应用于控制器类。例如,如果控制器类包含与整个会话相关的逻辑和数据,则使用 javax . enterprise . context . session scoped 对该类进行注释。但是,如果控制器类仅与请求级别相关,则使用 javax . enterprise . context . request scoped 对该类进行注释。根据此逻辑将每个不同的范围应用于控制器类。

它是如何工作的

控制器类范围可以完全改变应用运行的方式。控制器在范围内的时间长短会对应用的各个视图产生很大的影响。幸运的是,对不同的控制器应用不同的作用域是一件容易的事情。然而,编程方法会根据控制器类所处的范围而发生巨大的变化。CDI 提供了许多可以利用的示波器,如表 17-2 所示。

表 17-2。CDI 示波器
|

范围

|

持续时间

|
| --- | --- |
| @应用范围 | 状态在应用中的所有用户会话之间共享。 |
| @依赖 | 对象接收与客户端 bean 相同的生命周期。(默认范围) |
| @会话范围 | 开发人员控制对话的开始和结束,并且在整个对话过程中维护状态。 |
| @ RequestScoped | 状态持续单个 HTTP 请求的持续时间。 |
| @会话范围 | 状态在用户会话期间持续存在。 |
| javax.faces.view.ViewScoped | 只要 NavigationHandler 没有导致导航到不同的 viewId,状态就会持续。 |

如前所述,在应用作用域时要记住的一件事是,控制器选择的作用域会影响应用的其余部分。如果控制器将包含在整个用户会话中有用的数据,那么@SessionScoped 可能是最佳选择。请记住,@SessionScoped bean 中的所有数据都将在整个会话期间保留。因此,如果在 bean 中声明并填充了列表,则必须以编程方式刷新或更改 bean 的内容。如果使用某个范围导致 bean 在整个用户会话过程中被刷新,情况就不一样了。例如,如果同一个 bean 是@RequestScoped,那么列表中的数据将在每次发出请求时被重新查询和填充。

注意

范围还会对与其他受管 beans 的交互产生很大影响。注入相同范围的 beans 是很重要的

17-7.生成和应用模板

问题

您希望在应用的所有视图中应用相同的可视化模板。

解决办法

利用 Facelets 模板并应用于每个视图。要创建模板,必须首先开发一个新的 XHTML 视图文件,然后向其中添加适当的 HTML/JSF/ XML 标记。来自其他视图的内容将取代 ui:一旦模板被应用到一个或多个 JSF 视图,就在模板中插入元素。下面的源代码来自一个名为 template.xhtml 的模板,该模板将应用于 HelloJsf 应用中的所有视图:

<html 
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:p="http://primefaces.org/ui">

    <h:head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <h:outputStylesheet library="css" name="default.css"/>
        <h:outputStylesheet library="css" name="cssLayout.css"/>

        <title>Hello JSF</title>

    </h:head>

    <h:body>

        <p:growl id="growl" life="3000" />

        <p:layout fullPage="true">
            <p:layoutUnit position="north" size="65" header="#{bundle.AppName}">
                <h:form id="menuForm">
                    <p:menubar>
                        <p:menuitem value="Home" outcome="/index.xhtml" icon="ui-icon-home"/>

                           <p:menuitem value="Hello Main" outcome="/helloUser.xhtml" />
                              <p:menuitem value="PrimeFaces" outcome="/welcomePrimefaces.xhtml" />
                           <p:menuitem value="Hello Ajax" outcome="/helloAjax.xhtml" />

                    </p:menubar>
                </h:form>
            </p:layoutUnit>

            <p:layoutUnit position="south" size="60">
                <ui:insert name="footer"/>
            </p:layoutUnit>

            <p:layoutUnit position="center">
                <ui:insert name="content"/>
            </p:layoutUnit>

        </p:layout>

    </h:body>

</html>

模板定义了应用视图的整体结构。但是,它可以使用 CSS 样式表来声明模板中每个元素的格式。样式表应该包含在应用的资源目录中,以便视图可以访问它。也可以在模板中使用 JSF EL。如果使用 EL,通常由会话或应用范围的受管 bean 来驱动内容。模板的 JSF 客户端视图将包含视图内容周围的标签,以及属于模板内相应标签的标记命名段周围的标签。下面的视图是前面显示的模板的客户端视图的示例。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html 
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:p="http://primefaces.org/ui"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

      <ui:composition template="layout/template.xhtml">

        <ui:define name="content">
        Hello from Facelets
        <br />
        <h:link outcome="welcomePrimefaces" value="Primefaces welcome page" />
        <br/>
        <h:form>
            <p:messages id="messages"/>
            <br/>

            <p:outputLabel for="firstName" value="First: "/>
            <p:inputText id="firstName" value="#{helloJsfController.user.firstName}"/>
            <br/>
            <p:outputLabel for="lastName" value="Last: " />
            <p:inputText id="lastName" value="#{helloJsfController.user.lastName}"/>
            <br/>
            <p:outputLabel for="email" value="Email: " />
            <p:inputText id="email" value="#{helloJsfController.user.email}"/>
            <br/>
            <p:commandButton id="submitUser" value="Submit"
                             action="#{helloJsfController.createUser()}"
                             update="messages, helloUsers"/>

            <br/>
            <p:dataTable id="helloUsers" var="user" value="#{helloJsfController.helloUserList}">
                <p:column headerText="First Name">
                    <h:outputText value="#{user.firstName}"/>
                </p:column>
                <p:column headerText="Last Name">
                    <h:outputText value="#{user.lastName}"/>
                </p:column>
                <p:column headerText="Email">
                    <h:outputText value="#{user.email}"/>
                </p:column>
            </p:dataTable>

        </h:form>
        </ui:define>

    </ui:composition>

</html>

它是如何工作的

为了创建统一的应用体验,视图应该是一致的,因为它们看起来相似,并且以统一的方式运行。开发 web 页面模板的想法已经存在很多年了,但不幸的是,许多模板实现在每个应用页面上都包含重复的标记。虽然为每个单独的 web 页面复制相同的布局是可行的,但这造成了维护上的一场噩梦。当需要更新页眉中的单个链接时会发生什么?如果模板在每个页面上都被复制,这样的难题将导致开发者访问并手动更新应用的每个网页。Facelets 视图定义语言为视图模板的开发提供了一个健壮的解决方案,这是使用 JSF 技术的主要好处之一。

Facelets 提供了将单个模板应用于应用中一个或多个视图的能力。这意味着开发人员可以创建一个视图来构造页眉、页脚和模板的其他部分,然后这个视图可以应用于任何数量的负责包含主视图内容的其他视图。这种技术减轻了诸如更改页面标题中的单个链接之类的问题,因为现在模板可以用新的链接进行更新,应用中的每个其他视图都将自动反映这种更改。

要使用 Facelets 创建模板,请创建一个 XHTML 视图,声明所需的名称空间,然后相应地添加 HTML、JSF 和 Facelets 标记,以设计所需的布局。模板可以被认为是 web 视图的“外壳”,因为它可以包含任意数量的其他视图。同样,任意数量的 JSF 视图可以应用相同的模板,因此应用的整体外观将保持不变。

负责控制视图布局的 Facelets 标记。为了利用这些 Facelets 标记,您需要在模板的元素中声明 Facelets 标记库的 XML 名称空间。注意,这里还指定了标准 JSF 标记库的 XML 名称空间。

<html 
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:h="http://xmlns.jcp.org/jsf/html">
...

Facelets 包含许多特殊的标记,可以用来帮助控制页面流和布局。中的表 17-3 列出了对控制页面流和布局有用的 Facelets 标签。本例模板中使用的惟一 Facelets 标记是 ui:insert。ui:insert 标记包含一个 name 属性,该属性被设置为将包含在视图中的相应 ui:define 元素的名称。看看这个例子的源代码,您可以看到下面的 ui:insert tag:

表 17-3。Facelets 页面控件和模板标签
|

标签

|

描述

|
| --- | --- |
| 用户界面:组件 | 定义模板组件并指定组件的文件名 |
| ui:合成 | 定义页面组合并封装所有其他 JSF 标记 |
| 用户界面:调试 | 创建调试组件,该组件在呈现组件时捕获调试信息,即组件树的状态和应用中的作用域变量 |
| 不明确的 | 定义由模板插入页面的内容 |
| ui:装饰 | 页面的装饰部分 |
| ui:片段 | 定义一个模板片段,很像 ui:component,除了没有忽略标记之外的所有内容 |
| 用户界面:包括 | 允许在视图中封装和重用另一个 XHTML 页面 |
| 用户界面:插入 | 将内容插入模板 |
| ui:停止 | 将参数传递给包含的文件或模板 |
| ui:重复 | 迭代一组数据 |
| 用户界面:删除 | 从页面中删除内容 |

<ui:insert name="content">Content</ui:insert>

如果一个视图使用模板,也就是模板客户端,它必须在视图标签中列出模板。在中,视图必须指定一个与同名的标签,然后放置在开始和结束标签之间的任何内容都将被插入到视图的那个位置。但是,如果模板客户端不包含与标签同名的标签,那么将显示模板内开始和结束标签之间的内容。

摘要

Java EE web 应用的开发可能是一个非常大的主题,这一章只是触及了可以利用的许多技术中的一些。JSF web 框架是成熟和健壮的,它为开发复杂和易于使用的应用提供了许多选择。结合底层的 Java EE 技术,包括 EJB、JAX-RS、JPA 和其他技术,Java web 开发功能强大且易于上手。

十八、Nashorn 和脚本

在 Java 6 中,javax.script 包用于将脚本语言与 Java 结合在一起。它使开发人员能够将脚本语言编写的代码直接嵌入到 Java 应用中。这开创了新一代多语言应用,因为开发人员能够构建包含用 JavaScript 和 Python 等语言编写的脚本的 Java 解决方案。Java 6 中使用的 JavaScript 引擎叫做 Rhino。它是 JavaScript 引擎的一个实现,完全用 Java 开发。虽然它包含完整的 JavaScript 实现,但它是一个较旧的引擎,不再符合当前的 JavaScript 标准。

Java 8 引入了一个新的 JavaScript 引擎,叫做 Nashorn。它基于 ECMAScript-262 Edition 5.1 语言规范,支持 Java 6 中引入的 javax.script API。除了为 Java 平台带来一个现代的 JavaScript 引擎之外,Nashorn 还包含一些新特性,使得开发 JavaScript 和 Java 解决方案变得更加容易和健壮。名为 jjs 的新命令行工具提供了超越 jrunscript 的脚本功能。Nashorn 还可以完全访问 JavaFX 8 API,允许开发人员完全用 JavaScript 构建 JavaFX 应用。

JDK 9 通过在发布时包含一组来自 EMCAScript 6 规范的精选功能,进一步提高了 Nashorn 的可用性。随着时间的推移,EMCAScript 6 的更多功能可能会被纳入 JDK 9 的更新和 JDK 的后续版本中。

本章涉及使用 Nashorn 引擎来构建集成 Java 和 JavaScript 世界的解决方案。它没有涵盖 Nashorn 提供的所有功能,但是已经足够让您开始使用了。

18-1.从 Java 加载和执行 JavaScript

问题

您希望从 Java 应用中加载并执行 JavaScript 代码。

解决办法

使用 Nashorn 引擎执行 JavaScript,Nashorn 引擎是 Java 8 的下一代 JavaScript 引擎,用于执行 JavaScript 代码。可以调用 Nashorn 引擎来处理内嵌 JavaScript,或者直接在 Java 代码中处理外部 JavaScript 文件。使用 Java ScriptEngineManager 执行外部 JavaScript 文件或内嵌 JavaScript 代码。一旦获得了 ScriptEngineManager(),就获得了用于 JavaScript 代码执行的 Nashorn 引擎的实例。

在下面的示例中,Nashorn ScriptEngine 用于调用驻留在本地文件系统上的 JavaScript 文件。

public static void loadExternalJs(){
    ScriptEngineManager sem = new ScriptEngineManager();
    ScriptEngine nashorn = sem.getEngineByName("nashorn");
    try {
        nashorn.eval("load('src/org/java9recipes/chapter18/js/helloNashorn.js')");
    } catch (ScriptException ex) {
        Logger.getLogger(NashornInvoker.class.getName()).log(Level.SEVERE, null, ex);
    }
}

helloNashorn.js 文件中的代码如下:

print("Hello Nashorn!");

接下来,我们来看看一些内联 JavaScript。在下面的示例中,获取了一个 Nashorn ScriptEngine,然后创建了一个 JavaScript 函数来获取地下水池的加仑数。然后执行该函数以返回结果。

    public static void loadInlineJs(){
        ScriptEngineManager sem = new ScriptEngineManager();
        ScriptEngine nashorn = sem.getEngineByName("nashorn");
        try {
            nashorn.eval("function gallons(width, length, avgDepth){var volume =
                          avgDepth * width * length;" +
                          "return volume * 7.48; }");
            nashorn.eval("print('Gallons of water in pool: '+ gallons(16,32,5))");
        } catch (ScriptException ex) {
            Logger.getLogger(NashornInvoker.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

}

结果:

run:
Hello Nashorn!
Gallons of water in pool: 19148.800000000003

它是如何工作的

使用 Nashorn 引擎在 Java 应用中执行 JavaScript 有几种不同的方法。例如,可以从名为 jjs 的命令行界面(CLI)调用 Nashorn,或者可以使用 ScriptEngineManager。在这个菜谱中,示例涵盖了两种使用 Nashorn 执行 JavaScript 的技术,每种技术都需要使用 ScriptEngineManager,自 Java 6 以来,script engine manager 一直是 JDK 的一部分。要从 ScriptEngineManager 获取 Nashorn 引擎,首先创建 ScriptEngineManager 的新实例。一旦获得引擎,就可以通过将表示所需引擎的字符串值传递给 getEngineByName()方法来获得特定的引擎。在这种情况下,您传递名称 nashorn 来获得用于处理 JavaScript 的 Nashorn 引擎。获得 Nashorn 引擎后,您就可以通过调用引擎的 eval()方法来调用 JavaScript 文件或评估内联 JavaScript 代码了。

这个菜谱中的第一个代码示例演示了如何将一个 JavaScript 文件传递给引擎进行调用。本例中的 helloNashorn.js 包含一行 JavaScript,它打印一条消息,但不返回任何结果。执行一个. js 文件最困难的部分可能是必须确保该文件包含在类路径中,或者将文件的完整路径传递给 eval()方法。

第二个代码示例演示了如何编写和评估内联 JavaScript。首先,定义一个标识为加仑的函数,它接受三个参数,并根据池的宽度、长度和平均深度返回加仑数。在随后的 eval()调用中,调用该函数,传递参数并返回结果。本例中需要注意的重要一点是,尽管 JavaScript 跨越了多个 eval()调用,但范围是保持不变的,因此引擎中的每个 eval()调用都可以看到在之前的调用中创建的对象。

从 Java 6 开始,在 Java 代码中使用脚本语言已经成为可能。Nashorn 引擎的获取方式与其他引擎相同,通过传递一个字符串来按名称表示引擎。这个 JavaScript 引擎与以前的 rendition Rhino 的区别在于,新的 JavaScript 引擎速度更快,并且更好地符合 EMCA 标准化的 JavaScript 规范。自 JDK 8 更新 40 以来,更新的 EMCAScript 6 规范中的一些功能已经移植到 Nashorn 中。由于更新后的规范中有大量新功能,它们将随着时间的推移通过 JDK 的各个版本添加进来。JDK 9 引入了对 EMCAScript 6 特性的重要子集的支持。

18-2.通过命令行执行 JavaScript

问题

出于原型或执行目的,您希望通过命令行执行 JavaScript

解决方案 1

调用 jjs 工具,它是 Java 的一部分。要执行 JavaScript 文件,从命令行调用 jjs 工具,然后传递要执行的 JavaScript 文件的完全限定名(如果不在类路径中,则包括路径)。例如,要执行 helloNashorn.js,请使用以下命令:

jjs /src/org/java9recipes/chapter18/js/helloNashorn.js
Hello Nashorn!

要将参数传递给 JavaScript 文件进行处理,请以相同的方式调用脚本,但要包括尾随的破折号-,后跟要传递的参数。例如,以下代码位于名为 helloParameter.js 的文件中:

#! /usr/bin/env
var parameter = $ARG[0];
print(parameter ? "Hello ${parameter}!": "Hello Nashorn!");

使用以下命令调用这个 JavaScript 文件,传递参数 Oracle:

jjs /src/org/java9recipes/chapter18/js/helloParameter.js – Oracle

结果如下:

Hello Oracle!

jjs 工具也可以用作交互式解释器,只需不带任何选项地执行 jjs 即可。命令解释器允许您在完全交互式的 JavaScript 环境中工作。在下面的代码行中,jjs 工具被调用来打开一个命令 shell,并声明和执行一个函数。最后,退出命令 shell。

jjs
jjs> function gallon(width, length, avgDepth){return (avgDepth * width * length) * 7.48;}
function gallon(width, length, avgDepth){return (avgDepth * width * length) * 7.48;}
jjs> gallon(16,32,5)
19148.800000000003
jjs> exit()

解决方案 2

利用 JSR 223 jrunscript 工具来执行 JavaScript。要执行 JavaScript 文件,请从命令行调用 jrunscript 工具,并传递要执行的 JavaScript 文件的完全限定名(如果不在类路径中,则包括路径)。例如,要执行 helloNashorn.js,请使用以下命令:

jrunscript /src/org/java9recipes/chapter18/js/helloNashorn.js
Hello Nashorn!

也许您想以内联方式传递 JavaScript 代码,而不是执行 JavaScript 文件。在这种情况下,您可以使用–e 标志调用 jrunscript,并以内联方式传递脚本。

jrunscript -e "print('Hello Nashorn')"
Hello Nashorn
注意

如果使用 jrunscript 实用程序,则字符串插值不可用。因此,您必须使用串联来实现类似的效果。要了解更多关于字符串插值的信息,请参考配方 18-3。

与 jjs 类似,jrunscript 工具也接受传递给 JavaScript 文件进行处理的参数。要使用 jrunscript 工具传递参数,只需在调用脚本时将它们附加到命令中,每个参数用空格分隔。例如,要调用文件 helloParameter.js 并传递参数,请执行以下命令:

jrunscript src/org/java9recipes/chapter18/js/helloParameter.js Oracle

与 jjs 类似,jrunscript 工具可以执行交互式解释器,允许您动态开发和构建原型,如下图所示。

A323910_3_En_18_Figa_HTML.jpg

它是如何工作的

自从 Java SE 6 发布以来,使用来自 Java 的脚本语言已经成为可能。在这个菜谱中,演示了通过命令行或终端执行 JavaScript 的两种解决方案。在解决方案 1 中,您看到了 jjs 命令行工具,这是 Java 8 中的新功能。这个工具可以用来调用一个或多个 JavaScript 文件,或者启动一个交互式 Nashorn 解释器。在这个例子中,您了解了如何在传递参数和不传递参数的情况下调用 JavaScript 文件。您还了解了如何调用 jjs 作为交互式解释器。该工具包含几个有用的选项。要查看完整列表,请参考位于docs . Oracle . com/javase/9/docs/technotes/tools/windows/jjs . html的在线文档。jjs 工具是与 Nashorn 一起使用的理想工具,因为它包含比 jrunscript 工具更多的选项,jrunscript 工具在解决方案 2 中进行了演示。

jrunscript 工具是在 Java 6 中引入的,它允许您从命令行执行脚本或调用交互式解释器,类似于 jjs。不同之处在于,jrunscript 还允许您通过传递–l 标志以及脚本引擎名称来使用其他脚本语言。

jrunscript –l js myTest.js

jrunscript 工具也包含选项,但是与 jjs 提供的选项相比,它是有限的。要查看 jrunscript 的所有可用选项,请参考位于docs . Oracle . com/javase/9/docs/technotes/tools/windows/jrunscript . html的在线文档。

18-3.在字符串中嵌入表达式

问题

当通过 jjs 实用程序调用 JavaScript 时,您希望引用字符串中的表达式或值。

解决办法

当通过 jjs 工具将 Nashorn 用作 shell 脚本语言时,可以通过将表达式或值包含在双引号文本字符串中的美元符号$和花括号{}中,将它们嵌入到字符串中。以下 JavaScript 驻留在名为 recipe18_3.js 的文件中,它可以作为 shell 脚本由 jjs 工具执行。在这个例子中,字符串插值是有效的,因为通过将 shebang 作为第一行添加,脚本已经变得可执行。有关 shebang 的更多信息,请参考配方 18-10。

#! /usr/bin/env
function gallons(width, length, avgDepth){var volume = avgDepth * width * length;
                                         return volume * 7.48; }                                 
print("Gallons of water in pool: ${gallons(16,32,5)}");       

通过 jjs 执行 JavaScript 文件,如下所示:

jjs src/org/java9recipes/chapter18/js/recipe18_3.js
Gallons of water in pool: 19148.800000000003
注意

这个示例 JavaScript 文件不能从 ScriptEngineManager 运行,因为它包含一个 shebang(它是一个可执行脚本)。

它是如何工作的

当您使用 Nashorn 的 shell 脚本特性时,您可以通过用美元符号和花括号${...}.这个概念在 Unix 世界中被称为字符串插值,Nashorn 借用这个概念来简化开发用于评估和显示信息的 shell 脚本。字符串插值可以改变字符串的内容,用值替换变量和表达式。使用这个特性,很容易将变量的内容嵌入到字符串中,而不需要执行手动连接。

在这个食谱的例子中,一个存储在. js 文件中的脚本包含一个嵌入式表达式,它调用一个 JavaScript 函数来返回计算出的液体加仑数。这可能是现实世界场景中最有用的技术,但是在使用 jjs 工具作为交互式解释器时,也可以使用嵌入式表达式。

jjs -scripting
jjs> "The current date is ${Date()}"
The current date is Wed Apr 30 2014 23:44:41 GMT-0500 (CDT)
注意

如果您没有使用 jjs 的脚本特性,字符串插值将不可用。此外,文本字符串必须用双引号括起来,因为单引号中的字符串不会被插入。在示例中,shebang(!usr/bin/env)用于使脚本可执行,从而调用 jjs 的脚本特性。

18-4.传递 Java 参数

问题

您希望将 Java 参数传递给 JavaScript 来使用。

解决办法

利用 javax.script.SimpleBindings 实例为任何 Java 字段提供基于字符串的名称,然后将 SimpleBindings 实例传递给 JavaScript 引擎调用。在下面的例子中,一个 Java 字符串参数被传递给 Nashorn 引擎,然后通过 JavaScript 打印出来。

String myJavaString = "This is a Java parameter!";
SimpleBindings simpleBindings = new SimpleBindings();
simpleBindings.put("myString", myJavaString);
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine nashorn = sem.getEngineByName("nashorn");
nashorn.eval("print (myString)", simpleBindings);

结果如下:

This is a Java parameter!

在 SimpleBindings 实例中可以传递多个 Java 类型值。在下面的示例中,在单个 SimpleBindings 实例中传递三个浮点值,然后将它们传递给一个 JavaScript 函数。

float width = 16;
float length = 32;
float depth = 5;
SimpleBindings simpleBindings2 = new SimpleBindings();
simpleBindings2.put("globalWidth", width);
simpleBindings2.put("globalLength", length);
simpleBindings2.put("globalDepth", depth);
nashorn.eval("function gallons(width, length, avgDepth){var volume = avgDepth * width * length; "+
        "        return volume * 7.48; }   " +
        "print(gallons(globalWidth, globalLength, globalDepth));", simpleBindings2);

结果:

19148.800000000003

它是如何工作的

要将 Java 字段值传递给 JavaScript,请使用 javax.script.SimpleBindings 构造,它基本上是一个 HashMap,可用于将值绑定和传递给 ScriptEngineManager。当值以这种方式传递给 Nashorn 引擎时,它们可以作为 JavaScript 引擎中的全局变量来访问。

18-5.将返回值从 JavaScript 传递到 Java

问题

您希望调用一个 JavaScript 函数,并将结果返回给调用它的 Java 类。

解决办法

创建一个与 Nashorn 一起使用的 ScriptEngine,然后将 JavaScript 函数传递给它进行评估。接下来,从引擎创建一个 Invocable,然后调用它的 invokeFunction()方法,传递 JavaScript 函数的基于字符串的名称,以及要使用的参数数组。在下面的示例中,一个名为 gals 的 JavaScript 函数被传递给 ScriptEngine 进行评估,稍后将使用这种技术调用它。然后,它返回一个双精度值。

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");

// JavaScript code in a String
String gallonsFunction = "function gallons(width, length, avgDepth){var volume = avgDepth * width * length; "
        + " return volume * 7.48; } ";
try {
    // evaluate script
    engine.eval(gallonsFunction);
    double width = 16.0;
    double length = 32.0;
    double depth = 5.0;
    Invocable inv = (Invocable) engine;
    double returnValue = (double) inv.invokeFunction("gallons",
                                  new Double[]{width,length,depth});
    System.out.println("The returned value:" + returnValue);

} catch (ScriptException | NoSuchMethodException ex) {
    Logger.getLogger(Recipe18_5.class.getName()).log(Level.SEVERE, null, ex);
}

结果如下:

run:
The returned value:19148.800000000003

在下面的示例中,调用了一个 JavaScript 文件并返回一个字符串值。JavaScript 文件的名称是 recipe18_5.js,其内容如下:

function returnName( name){
    return "Hello " + name;
}

接下来,使用 ScriptEngine 创建一个 Invocable 并调用外部 JavaScript 文件中的 JavaScript 函数。

engine.eval("load('/path-to/src/org/java9recipes/chapter18/recipe18_05/js/recipe18_5.js')");
Invocable inv2 = (Invocable) engine;
String returnValue2 = (String) inv2.invokeFunction("returnName", new String[]{"Nashorn"});
System.out.println("The returned value:" + returnValue2);

它是如何工作的

嵌入式脚本最有用的特性之一是能够将通过脚本引擎调用的代码与 Java 应用集成在一起。为了有效地集成脚本引擎代码和 Java 代码,这两者必须能够相互传递值。这个菜谱涵盖了将值从 JavaScript 返回到 Java 的概念。为此,设置一个 ScriptEngine,然后将其强制转换为 javax . script . invokable 对象。然后,可以使用 Invocable 对象来执行脚本函数和方法,从这些调用中返回值。

Invocable 对象使您能够执行指定的 JavaScript 函数或方法,并将值返回给调用者。Invocable 还可以返回一个接口,该接口将提供调用脚本对象的成员函数的方法。为了提供这个功能,Invocable 对象包含了几个方法(见表 18-1 )。

表 18-1。可调用的方法
|

方法

|

描述

|
| --- | --- |
| getInterface(类) | 使用解释器编译的函数返回接口的实现。 |
| getInterface(对象,类) | 使用已在解释器中编译的脚本对象的成员函数返回接口的实现。 |
| invokeFunction(字符串,对象) | 对顶级过程和函数的调用。返回一个对象。 |
| invokeFunction(对象,字符串,对象) | 调用上一次执行期间编译的脚本对象的方法。 |

在生成 Invocable 之前,JavaScript 文件或函数必须由 ScriptEngine 进行评估。该示例演示了如何调用 eval()方法来计算内联 JavaScript 函数(名为 gallonsFunction 的字符串),以及如何计算外部 JavaScript 文件。调用 eval()方法后,可以将 ScriptEngine 强制转换为 Invocable 对象,如下所示:

Invocable inv = (Invocable) engine;

然后可以调用 Invocable 来执行被评估的脚本代码中的函数或方法。表 18-1 列出了可以使用的调用方法。

在这个菜谱的例子中,invokeFunction 方法用于调用脚本中包含的函数。invokeFunction 的第一个参数是被调用函数的基于字符串的名称,第二个参数是作为参数传递的对象列表。Invocable 从 JavaScript 函数调用中返回一个对象,该对象可以被强制转换为适当的 Java 类型。

在 Java 和 ScriptEngine 实例之间共享值非常有用。在现实生活中,调用外部 JavaScript 文件并能够在 Java 代码和脚本之间来回传递值可能非常有用。如果需要,可以修改底层 JavaScript 文件,而无需重新编译应用。当您的应用包含一些需要不时更改的业务逻辑时,这种情况会非常有用。假设您有一个规则处理器,可以用来计算字符串,并且规则在不断发展。在这种情况下,可以将规则引擎编写为外部 JavaScript 文件,从而实现对该文件的动态更改。

18-6.使用 Java 类和库

问题

您希望在 Nashorn 解决方案中调用 Java 类和库。

解决办法

使用 Java.type()函数创建基于 Java 类或库的 JavaScript 对象。将您想要使用的 Java 类的基于字符串的全限定名称传递给该函数,并将其赋给一个变量。下面的代码表示一个名为 Employee 的 Java 对象,它将在这个应用中通过一个 JavaScript 文件使用。

package org.java9recipes.chapter18.recipe18_06;

import java.util.Date;
public class Employee {
    private int age;
    private String first;
    private String last;
    private String position;
    private Date hireDate;

    public Employee(){

    }

    public Employee(String first,
                    String last,
                    Date hireDate){
        this.first = first;
        this.last = last;
        this.hireDate = hireDate;
    }

    /**
     * @return the first
     */
    public String getFirst() {
        return first;
    }

    /**
     * @param first the first to set
     */
    public void setFirst(String first) {
        this.first = first;
    }

    /**
     * @return the last
     */
    public String getLast() {
        return last;
    }

    /**
     * @param last the last to set
     */
    public void setLast(String last) {
        this.last = last;
    }

...
}

接下来,让我们看看使用 Employee 类的 JavaScript 文件。这段 JavaScript 代码创建了几个雇员实例,然后将它们打印出来。它还使用 java.util.Date 类来演示标准 java 类的使用。

var oldDate = Java.type("java.util.Date");
var array = Java.type("java.util.ArrayList");
var emp = Java.type("org.java9recipes.chapter18.recipe18_06.Employee");

var empArray = new array();
var emp1 = new emp("Josh", "Juneau", new oldDate());
var emp2 = new emp("Joe", "Blow", new oldDate());
empArray.add(emp1);
empArray.add(emp2);
empArray.forEach(function(value, index, ar){
    print("Employee: " + value);
    print("Hire Date: " + value.hireDate);
});

最后,使用 ScriptEngineManager 执行 JavaScript 文件:

ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine nashorn = sem.getEngineByName("nashorn");
try {
    nashorn.eval("load('/path-to/employeeFactory.js');");
} catch (ScriptException ex) {
    Logger.getLogger(NashornInvoker.class.getName()).log(Level.SEVERE, null, ex);
}

结果如下:

Employee: Josh Juneau
Hire Date: Thu April 24 23:03:53 CDT 2016
Employee: Joe Blow
Hire Date: Fri April 25 12:00:00 CDT 2016

它是如何工作的

在 Nashorn 解决方案中使用 Java 类和库是非常自然的。该配方中的示例演示了如何使用专门为定制应用生成的 Java 类,以及如何使用 Java SE 中的 Java 类和库。为了使这些类对 JavaScript 可用,您必须从 JavaScript 内部调用 Java.type 函数,并传递要使用的基于字符串的 Java 类的完全限定名。Java.type 函数返回 Java 类型的 JavaScript 引用。在下面的示例摘录中,java.util.Date、java.util.ArrayList 和 Employee 类可用于使用这种技术的 JavaScript。

var oldDate = Java.type("java.util.Date");
var array = Java.type("java.util.ArrayList");
var emp = Java.type("org.java9recipes.chapter18.recipe18_06.Employee");

一旦 JavaScript 可以使用这些类型,就可以以类似于 Java 的方式调用它们。例如,在示例中,new oldDate()用于实例化 java.util.Date 的新实例。一个重要的区别是,您不使用 getters 和 setters 来调用 Java 属性。相反,您省略了方法的“get”或“set”部分,以小写字母开始字段名,从而直接调用字段。这使得从 JavaScript 中访问属性变得非常容易,并且更有效率和可读性。从脚本中的 forEach 循环可以看到这种访问的示例。要访问 employee hireDate 属性,只需调用 employee.hireDate 而不是 employee.getHireDate()。

从 JavaScript 中无缝访问 Java 的能力使得创建无缝的 Java 和 JavaScript 集成成为可能。

18-7.在 Nashorn 中访问 Java 数组和集合

问题

您需要从 Nashorn 解决方案中访问 Java 数组或集合。

解决办法

使用 Java.type 函数将 Java 数组强制转换为 JavaScript。一旦被强制,您可以通过调用 new 来实例化数组,然后通过按数字指定成员的索引来访问成员。在下面的例子中,Java int 数组类型是在 JavaScript 中创建的,然后它被实例化并用于存储。

jjs> var intArray = Java.type("int[]");
jjs> var intArr = new intArray(5);
jjs> intArr[0] = 0;
0
jjs> intArr[1] = 1;
1
jjs> intArr[0]
0
jjs> intArr.length
5

使用集合非常相似。要访问 Java 集合类型,需要调用 Java.type 函数,传递要创建的类型的基于字符串的名称。一旦获得了类型引用,就可以从 JavaScript 实例化和访问它。

jjs> var ArrayList = Java.type("java.util.ArrayList")
jjs> var array = new ArrayList();
jjs> array.add('hi');
true
jjs> array.add('bye');
true
jjs> array
[hi, bye]
jjs> var map = Java.type("java.util.HashMap")
jjs> var jsMap = new map();
jjs> jsMap.put(0, "first");
null
jjs> jsMap.put(1, "second");
null
jjs> jsMap.get(1);
second

它是如何工作的

要在 JavaScript 中使用 Java 数组和集合,需要调用 Java.type()函数并传递想要访问的 Java 类型的名称,将其赋给一个 JavaScript 变量。然后,JavaScript 变量可以被实例化,并以与在 Java 代码中使用 Java 类型相同的方式被利用。这个菜谱中的例子演示了如何从 JavaScript 中访问 Java 数组、数组列表和 HashMaps。

当使用 JavaScript 中的 Java 数组类型时,数组的类型必须传递给 Java.type()函数,包括一组空括号。一旦获得了类型并将其分配给 JavaScript 变量,就可以通过在括号中包含数组的静态大小来实例化它,就像在 Java 语言中实例化数组一样。类似地,可以通过指定索引来分配和检索数组中的值,从而访问数组。要返回并将一个 JavaScript 数组传递给 Java,可以使用 Java.to()函数,将 JavaScript 数组传递给它的 Java 类型的对应物。在下面的代码中,JavaScript 字符串数组被强制转换为 Java 类型。

jjs> var strArr = ["one","two","three"]
jjs> var javaStrArr = Java.type("java.lang.String[]");
jjs> var javaArray = Java.to(strArr, javaStrArr);
jjs> javaArray[1];
two
jjs> javaArray.class
class Ljava.lang.String;

集合与数组非常相似,因为必须使用 Java.type()函数来获取 Java 类型并将其赋给 JavaScript 变量。然后实例化该变量,并以与 Java 语言中相同的方式访问集合类型。

18-8.实现 Java 接口

问题

您希望利用 Nashorn 解决方案中的 Java 接口。

解决办法

创建接口的新实例,传递由属性组成的 JavaScript 对象。JavaScript 对象属性将实现接口中定义的方法。在下面的例子中,一个用于声明雇员职位类型的接口在一个 JavaScript 文件中实现。该示例演示自定义方法的实现,以及默认方法的使用。下面的代码是将在 JavaScript 中实现的接口 PositionType。

import java.math.BigDecimal;

public interface PositionType {

    public double hourlyWage(BigDecimal hours, BigDecimal wage);

    /**
     * Hourly salary calculation
     * @param wage
     * @return
     */
    public default BigDecimal yearlySalary(BigDecimal wage){
        return (wage.multiply(new BigDecimal(40))).multiply(new BigDecimal(52));
    }
}

接下来,让我们看看 JavaScript 文件中实现 PositionType 接口的代码。

var somePosition = new org.java9recipes.chapter18.recipe18_08.PositionType({
  hourlyWage: function(hours, wage){
      return hours * wage;
  }
});

print(somePosition instanceof Java.type("org.java9recipes.chapter18.recipe18_08.PositionType"));
var bigDecimal = Java.type("java.math.BigDecimal");

print(somePosition.hourlyWage(new bigDecimal(40), new bigDecimal(12.75)));

它是如何工作的

在 JavaScript 中使用 Java 接口有利于创建符合实现标准的对象。然而,在 JavaScript 中使用接口与在 Java 解决方案中使用接口有点不同。例如,接口不能在 Java 中实例化。在 JavaScript 中使用它们时,情况并非如此;您必须实例化接口类型的对象才能使用它。

该示例演示了 PositionType 接口的实现,该接口用于在雇员职位中定义许多方法。这些方法用于计算雇员的时薪和年薪。为了利用 JavaScript 中的 PositionType 接口,new 关键字用于实例化该接口的一个实例,并将其分配给一个 JavaScript 变量。当实例化接口时,一个 JavaScript 对象被传递给构造函数。对象包含接口中每个非默认方法的实现,方法是标识方法的名称,后跟实现。在该示例中,在实例化上只实现了一个方法,它被标识为 hourlyWage()。如果实现了一个以上的方法,那么这些实现将用逗号分隔。

尽管在 JavaScript 中使用 Java 接口有点不同,但它们确实有好处。实际上,他们在 JavaScript 中执行的任务和在 Java 中一样。在 Java 中,为了实现一个接口,你必须创建一个实现它的对象。您在 JavaScript 中做同样的事情,除了为了创建实现对象,您必须实例化接口的一个实例。

18-9.扩展 Java 类

问题

您希望在 Nashorn JavaScript 解决方案中扩展一个具体的 Java 类。

解决办法

首先,通过调用 JavaScript 文件中的 Java.type()函数,获得要扩展的 Java 类的引用。然后通过调用 Java.extend()函数创建子类,并将引用传递给将要扩展的类,以及包含将要更改的实现的 JavaScript 对象。

下面的代码是 Employee 类的代码,稍后将从一个 JavaScript 文件中扩展。

package org.java9recipes.chapter18.recipe18_09;

import java.math.BigDecimal;
import java.util.Date;

public class Employee {
    private int age;
    private String first;
    private String last;
    private String position;
    private Date hireDate;

    ...

    public BigDecimal grossPay(BigDecimal hours, BigDecimal rate){
        return hours.multiply(rate);
    }
}

下面是用于扩展和使用该类的 JavaScript 代码:

var Employee = Java.type("org.java9recipes.chapter18.recipe18_09.Employee");
var bigDecimal = Java.type("java.math.BigDecimal");
var Developer = Java.extend(Employee, {
    grossPay: function(hours, rate){
        var bonus = 500;
        return hours.multiply(rate).add(new bigDecimal(bonus));
    }
});

var javaDev = new Developer();
javaDev.first = "Joe";
javaDev.last = "Dynamic";
print(javaDev + "'s gross pay for the week is: " + javaDev.grossPay(new bigDecimal(60),
                                                                    new bigDecimal(80)));

结果如下:

Joe Dynamic's gross pay for the week is: 5300

它是如何工作的

要从 JavaScript 中扩展一个标准的 Java 类,您需要调用 Java.extend()函数,传递您想要扩展的 Java 类,以及一个 JavaScript 对象,该对象包含将在子类中更改的任何字段或函数。对于这个菜谱中的例子,扩展了一个名为 Employee 的 Java 类。然而,同样的技术可以用来扩展任何其他 Java 接口,比如 Runnable、Iterator 等等。

在本例中,为了获得 JavaScript 中的 Employee 类,调用了 Java.type()函数,并传递了完全限定的类名。从调用中收到的对象存储在名为 Employee 的 JavaScript 变量中。接下来,通过调用 Java.extend()函数并传递 Employee 类以及一个 JavaScript 对象来扩展该类。在该示例中,发送给 Java.extend()函数的 JavaScript 对象包括 Employee 类 grossPay()方法的不同实现。从 Java.extend()函数返回的对象然后被实例化,并通过 JavaScript 访问。

当您使用 Nashorn 解决方案时,在 JavaScript 中扩展 Java 类可能是一个非常有用的特性。共享 Java 对象的能力使得访问现有的 Java 解决方案并在其上构建成为可能。

18-10.在 Unix 中创建可执行脚本

问题

您希望让您的 JavaScript 文件成为可执行文件。

解决办法

通过添加一个 shebang(!)作为脚本的第一行,后面是 jjs 可执行文件的位置路径。在下面的例子中,一个非常简单的 JavaScript 文件通过包含一个 shebang 变得可执行,它指向 jjs 工具的符号链接。

#! /usr/bin/env jjs
print('I am an executable');

要执行脚本,必须给它适当的权限。应用 chmod a+x 权限(在 Unix 中)使脚本可执行。

chmod a+x src/org/java9recipes/chapter18/recipe18_10/jsExecutable.js

该脚本现在可以作为可执行文件调用,如以下命令所示:

Juneau$ ./src/org/java9recipes/chapter18/recipe18_10/jsExecutable.js
I am an executable

它是如何工作的

要使脚本可执行,只需在第一行添加一个 shebang。shebang 在基于 Unix 的操作系统中用来告诉程序加载器,脚本的第一行应该被视为解释器指令,脚本应该被传递给解释器执行。在这个方法的解决方案中,脚本的第一行告诉程序加载器应该使用 jjs 工具执行脚本的内容:

#! /usr/bin/env jjs

通过以这种方式调用 jjs 工具,脚本选项被自动启用,允许您在脚本中利用脚本特性。以下列表包括在启用脚本选项的情况下通过 jjs 执行时可以使用的额外脚本功能:

  • 字符串插值:(见配方 18-3)

    var threeyr = 365 * 3;
    print("The number of days in three years is ${threeyr}");
    
  • Shell 调用:调用外部程序的能力

  • 可以使用特殊的环境变量(\(ARG 和\)ENV)

用 JavaScript 开发可执行脚本的能力非常强大。不仅 JavaScript 世界唾手可得,而且整个 Java 世界都是可用的,因为您可以将 Java 类和库导入到脚本中。

18-11.用 Nashorn 实现 JavaFX

问题

您希望使用 JavaScript 实现一个 Java GUI。

解决方案 1

使用 JavaScript 开发一个 JavaFX 应用,并将其存储在 JavaScript 文件中。使用 jjs 工具和–FX 选项调用该文件。以下代码是用 JavaScript 编写的 JavaFX 应用。JavaFX 应用可用于收集汽车数据。

var ArrayList = Java.type("java.util.ArrayList");
var Scene = javafx.scene.Scene;
var Button = javafx.scene.control.Button;
var TextField = javafx.scene.control.TextField;
var GridPane = javafx.scene.layout.GridPane;
var Label = javafx.scene.control.Label;
var TextArea = javafx.scene.control.TextArea;

var carList = new ArrayList();
var carCount = "There are currently no cars";
var car = {
    make:"",
    model:"",
    year:"",
    description:""
};
print(carCount);
function start(primaryStage) {

    primaryStage.title="Car Form JS Demo";

    var grid = new GridPane();
    grid.hgap = 10;
    grid.vgap = 10;

    var makeLabel = new Label("Make:");
    grid.add(makeLabel, 0, 1);

    var makeText = new TextField();
    grid.add(makeText, 1, 1);

    var modelLabel = new Label("Model:");
    grid.add(modelLabel, 0, 2);

    var modelText = new TextField();
    grid.add(modelText, 1, 2);

    var yearLabel = new Label("Year:");
    grid.add(yearLabel, 0, 3);

    var yearText = new TextField();
    grid.add(yearText, 1, 3);

    var descriptionLabel = new Label("Description:");
    grid.add(descriptionLabel, 0, 4);

    var descriptionText = new TextArea();
    grid.add(descriptionText, 1, 4);

    var button = new Button("Add Car");
    button.onAction = function(){
        print("Adding Car:" + makeText.text);
        car.make=makeText.text;
        car.model=modelText.text;
        car.year=yearText.text;
        car.description=descriptionText.text;
        carList.add(car);   
        carCount = "The number of cars is: "+ carList.size();
        print(carCount);
    };
    grid.add(button, 0,5);

    primaryStage.scene = new Scene(grid, 800, 500);
    primaryStage.show();
}

最终的应用如图 [18-1 所示。

A323910_3_En_18_Fig1_HTML.jpg

图 18-1。用 JavaScript 编写的 JavaFX 应用

解决方案 2

使用 Java 编写 JavaFX 应用,并使用 ScriptEngine 嵌入 JavaScript 应用实现。下面的 Java 类称为 CarCollector.java,它实现 Java FX . application . application。Java 类实现 start()方法,该方法包含一个 ScriptEngine 来嵌入实现应用的 JavaScript 代码。

package org.java9recipes.chapter18.recipe18_11;

import java.io.FileReader;
import javafx.application.Application;
import javafx.stage.Stage;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class CarCollector extends Application {

    private final String SCRIPT = getClass().getResource("carCollector.js").getPath();

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

    @Override
    public void start(Stage stage) {
        try {
            ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
            engine.put("primaryStage", stage);
            engine.eval(new FileReader(SCRIPT));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接下来,让我们看一下名为 carCollector.js 的 JavaScript 文件,它实现了该应用。注意,代码不包含 start()函数,因为应用 start()方法已经在 Java 代码中实现了。JavaScript 文件仅仅包含实现。

var ArrayList = Java.type("java.util.ArrayList");
var Scene = javafx.scene.Scene;
var Button = javafx.scene.control.Button;
var TextField = javafx.scene.control.TextField;
var GridPane = javafx.scene.layout.GridPane;
var Label = javafx.scene.control.Label;
var TextArea = javafx.scene.control.TextArea;

var carList = new ArrayList();
var carCount = "There are currently no cars";
var car = {
    make: "",
    model: "",
    year: "",
    description: ""
};
print(carCount);

primaryStage.title = "Car Form JS Demo";

var grid = new GridPane();
grid.hgap = 10;
grid.vgap = 10;

var makeLabel = new Label("Make:");
grid.add(makeLabel, 0, 1);

var makeText = new TextField();
grid.add(makeText, 1, 1);

var modelLabel = new Label("Model:");
grid.add(modelLabel, 0, 2);

var modelText = new TextField();
grid.add(modelText, 1, 2);

var yearLabel = new Label("Year:");
grid.add(yearLabel, 0, 3);

var yearText = new TextField();
grid.add(yearText, 1, 3);

var descriptionLabel = new Label("Description:");
grid.add(descriptionLabel, 0, 4);

var descriptionText = new TextArea();
grid.add(descriptionText, 1, 4);

var button = new Button("Add Car");
button.onAction = function() {
    print("Adding Car:" + makeText.text);
    car.make = makeText.text;
    car.model = modelText.text;
    car.year = yearText.text;
    car.description = descriptionText.text;
    carList.add(car);
    carCount = "The number of cars is: " + carList.size();
    print(carCount);
};
grid.add(button, 0, 5);

primaryStage.scene = new Scene(grid, 800, 500);
primaryStage.show();

它是如何工作的

Nashorn 引擎可以完全访问 JavaFX API。这意味着可以构建完全或部分用 JavaScript 编写的 JavaFX 应用。这个配方的两个解决方案演示了每一种技术。第一个解决方案演示了如何完全用 JavaScript 开发 JavaFX 应用。当您使用解决方案 1 中演示的技术时,可以通过使用 jjs 工具并指定–FX 选项来执行 JavaScript 实现,如下所示:

jjs –fx recipe18_11.js

解决方案 2 演示了如何从 Java 代码构建一个 JavaFX 应用,嵌入用 JavaScript 编写的实现代码。要使用这种技术,可以通过扩展 javafx.application.Application 类并覆盖 start()方法来构造一个标准的 JavaFX 应用类。在 start()方法中,创建一个 Nashorn ScriptEngine 对象,并使用它嵌入一个包含应用实现的 JavaScript 文件。在调用引擎的 eval()方法并传递 JavaScript 文件之前,使用引擎的 put()方法将 JavaFX 阶段传递给引擎。

engine.put("primaryStage", stage);

深入研究一下 JavaScript 代码,任何 JavaFX API 类都可以通过使用 Java.type()函数并传递完全限定的类名来导入。将导入的类分配给 JavaScript 变量,稍后将在应用构造中使用这些变量。完全用 JavaScript 编写时,必须创建一个 start()函数来包含 JavaFX 应用阶段构造。另一方面,当您使用 Java 代码启动应用时,没有必要创建 start()函数。在这个示例中,GridPane 布局用于构建一个捕获汽车数据的表单。每个表单域都由一个标签和一个 TextField 或 TextArea 构成。单击按钮时,汽车数据存储在 JavaScript 对象中。

关于这两种实现中的 JavaScript 代码,有一些事情需要注意。语法与 Java 代码略有不同,因为没有使用 getters 和 setters。此外,按钮操作处理程序的实现是一个简单的 JavaScript 函数。

使用 JavaScript 构建 JavaFX 应用可能是使用 Java 代码的有趣替代方案。语法有使用以前的 JavaFX Script 语言的感觉,比 Java 稍微简单一点。如果使用完整的 JavaScript 实现,不用重新编译就能更改应用也是一件好事。

18-12.利用 ECMAScript6 功能

问题

您希望利用 ECMAScript6 的一些特性,比如模板字符串、更多的作用域选项和新的循环结构。

解决办法

利用 Java 9 中 ECMAScript6 新特性的子集。初始版本包括 ECMAScript6 新特性的一小部分,但是该特性集将随着后续 Java 9 版本的发布而扩展。

要利用这些新功能,请使用本章前面的方法中描述的解决方案之一,使用更新的 ECMAScript6 语法。在这个菜谱中,打开 jjs 实用程序并输入下面的例子来查看新的特性。

模板字符串功能的工作原理是允许字符串包含动态变量,这样变量就可以改变,从而改变字符串的文本。以下示例演示了如何利用模板字符串:

jjs> var customer = {name:"Josh"}
jjs> var message = `Hello ${customer.name}`

ECMAScript 中添加了 let 关键字,允许使用块范围的变量:

let name = "Josh";
console.log("first: " + name)
if (name.length > 1){
    let name = "Duke";
    console.log(name);
}
console.log(name);

输出:

first: Josh
Duke
Josh

ECMAScript6 包含新的循环结构,例如 for-in:

var names = ['Josh', 'Duke']
for (var x of names){
    console.log(x);
}

它是如何工作的

ECMAScript 6 中有许多新特性,其中一些特性是 Java 9 中 Nashorn 的一部分。事实上,一些新特性进入了 Java 8,Update 40,那些是 letconstblock scope 。新特性的列表如此之大,以至于试图在一个版本中把它们都放入 Nashorn 是一项艰巨的任务。因此,Nashorn for Java 9 的初始版本包含了新特性的另一个子集,在后续的 Java 9 版本中将会添加更多的新特性。

Java 9 的初始版本包含以下新的 Nashorn ECMAScript 6 特性:

  • 模板字符串

  • let、const 和 block 范围

  • 迭代器..循环的

  • 地图、集合、武器地图和武器集合

  • 标志

  • 二进制和八进制文字

Java 9 中 Nashorn 引擎的未来版本计划提供以下特性:

  • 箭头功能

  • 增强的对象文字

  • 解构分配

  • 默认、静止和展开参数

  • 统一码

  • 子类内置

  • 承诺

  • 委托书

  • 数学、数字、字符串和对象 API

  • 反射 API

摘要

Nashorn 使开发人员能够利用 Java 生态系统中的现代 JavaScript 功能。Nashorn 引擎可以完全访问所有 Java APIs,包括 JavaFX。新的 jjs 工具提供了脚本功能,允许开发人员创建完全用 JavaScript 编写的可执行脚本。最后,我们介绍了 ECMAScript6 的一些新特性,这些特性已经被添加到 Java 9 中的 Nashorn 引擎中。

十九、电子邮件

电子邮件通知是当今企业系统不可或缺的一部分。Java 通过提供 JavaMail API 来支持电子邮件通知。使用这个 API,您可以发送电子邮件来响应一个事件(比如一个完整的表单或一个最终的脚本)。您还可以使用 JavaMail API 来检查 IMAP 或 POP3 邮箱。

要遵循本章中的方法,请确保您已经设置了允许电子邮件通信的防火墙。大多数情况下,防火墙允许与电子邮件服务器进行出站通信而不会出现问题,但是如果您运行自己的本地 SMTP(电子邮件)服务器,您可能需要配置防火墙以允许电子邮件服务器正常运行。

注意

JavaMail API 包含在 Java EE 下载中。如果您正在使用 Java SE,您将需要下载并安装 JavaMail API。

19-1.安装 JavaMail

问题

您希望安装 JavaMail,供您的应用在发送电子邮件通知时使用。

解决办法

从 Oracle 的 JavaMail 网站下载 JavaMail。目前,您需要的下载位于

www.oracle.com/technetwork/java/javamail/

下载后,解压并添加 JavaMail。jar 文件作为项目的依赖项(mail.jar 和 lib*)。罐子)。

它是如何工作的

JavaMail API 包含在 Java EE SDK 中,但是如果您正在使用 Java SE SDK,则需要下载 JavaMail API 并将其添加到您的 Java SE 项目中。通过下载和添加依赖项,您可以访问健壮的电子邮件 API,该 API 允许您发送和接收电子邮件。

注意

如果您使用的不是 Java SE 6 或更高版本,您还需要 JavaBeans 激活框架(JAF)来使用 JavaMail。它包含在 Java SE 6 和更新的版本中。

19-2.发送电子邮件

问题

您需要您的应用来发送电子邮件。

解决办法

使用 Transport()方法,可以向特定的收件人发送电子邮件。在此解决方案中,通过 smtp.somewhere.com 服务器构建并发送电子邮件:

private void start() {
    Properties properties = new Properties();
    properties.put("mail.smtp.host", "smtp.somewhere.com");
    properties.put("mail.smtp.auth", "true");

    Session session = Session.getDefaultInstance(properties, new MessageAuthenticator("username","password"));

    Message message = new MimeMessage(session);
    try {
        message.setFrom(new InternetAddress("someone@somewhere.com"));
        message.setRecipient(Message.RecipientType.TO, new InternetAddress("someone@somewhere.com"));
        message.setSubject("Subject");
        message.setContent("This is a test message", "text/plain");
        Transport.send(message);
    } catch (MessagingException e) {
        e.printStackTrace();
    }
}

class MessageAuthenticator extends Authenticator {
    PasswordAuthentication authentication = null;

    public MessageAuthenticator(String username, String password) {
        authentication = new PasswordAuthentication(username,password);
    }

    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
        return authentication;
    }
} 

它是如何工作的

要利用 JavaMail API,首先要创建一个作为标准 Map 对象的 Properties 对象(实际上,它是从它继承而来的),在这个对象中放置 JavaMail 服务可能需要的不同属性。主机名是使用 mail.smtp.host 属性设置的,如果主机需要身份验证,则必须将 mail.smtp.auth 属性设置为 true。配置完 properties 对象后,获取一个 javax.mail.Session,它将保存电子邮件消息的连接信息。

创建会话时,如果服务需要鉴定,您可以指定登录信息。当连接到局域网之外的 SMTP 服务时,这可能是必要的。要指定登录信息,您必须创建一个 Authenticator 对象,它将包含 getPasswordAuthentication()方法。在本例中,有一个标识为 MessageAuthenticator 的新类,它扩展了 Authenticator 类。通过使 getPasswordAuthentication()方法返回 PasswordAuthentication 对象,可以指定用于 SMTP 服务的用户名/密码。

Message 对象表示实际的电子邮件,并公开电子邮件属性,如发件人/收件人/主题和内容。设置这些属性后,调用 Transport.send()静态方法发送电子邮件。

小费

如果不需要身份验证信息,可以调用 session . getdefaultinstance(properties,null)方法,为 Authenticator 参数传递一个 null。

19-3.将文件附加到电子邮件中

问题

您需要在电子邮件中附加一个或多个文件。

解决办法

创建包含不同部分的消息(称为多部分消息)可以让您发送文件和图像等附件。您可以指定电子邮件的正文和附件。包含不同部分的邮件被称为多用途 Internet 邮件扩展(MIME)邮件。它们在 javax.mail API 中由 MimeMessage 类表示。以下代码创建了这样一条消息:

Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject("Subject");

// Create Mime "Message" part
MimeBodyPart messageBodyPart = new MimeBodyPart();
messageBodyPart.setContent("This is a test message", "text/plain");

// Create Mime "File" part
MimeBodyPart fileBodyPart = new MimeBodyPart();
fileBodyPart.attachFile("<path-to-attachment>/attach.txt");

MimeBodyPart fileBodyPart2 = new MimeBodyPart();
fileBodyPart2.attachFile("<path-to-attachment>/attach2.txt");

// Piece the body parts together
Multipart multipart = new MimeMultipart();
multipart.addBodyPart(messageBodyPart);
multipart.addBodyPart(fileBodyPart);
//add another body part to supply another attachment
multipart.addBodyPart(fileBodyPart2);

// Set the content of the message to be the MultiPart
message.setContent(multipart);
Transport.send(message);

它是如何工作的

在 JavaMail API 中,您可以创建一个 MIME 电子邮件。这种类型的消息允许它包含不同的正文部分。在该示例中,生成了一个纯文本正文部分(包含电子邮件显示的文本),然后创建了两个附件正文部分,其中包含您要发送的附件。根据附件的类型,Java API 将自动为附件正文部分选择合适的编码。

在创建了每个正文部分之后,通过创建一个多部分对象并将每个单独的部分(纯文本和附件)添加到其中,可以将它们组合在一起。一旦多部分对象被组装成包含所有部分,它就被指定为 MimeMessage 的内容并被发送(就像配方 19-2 中一样)。

19-4.发送 HTML 电子邮件

问题

您想要发送包含 HTML 的电子邮件。

解决办法

您将电子邮件的内容类型指定为 text/html,并将 html 字符串作为邮件正文发送。在下面的示例中,使用 HTML 内容构造了一封电子邮件,然后发送出去。

MimeMessage message = new MimeMessage(session);
try {
    message.setFrom(new InternetAddress(from));
    message.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
    message.setSubject("Subject Test");

    // Create Mime Content
    MimeBodyPart messageBodyPart = new MimeBodyPart();
    String html = "<H1>Important Message</H1>" +
                  "<b>This is an important message...</b>"+
                  "<br/><br/>" +
                  "<i>Be sure to code your Java today!</i>" +
                  "<H2>It is the right thing to do!</H2>";
    messageBodyPart.setContent(html, "text/html; charset=utf-8");

    MimeBodyPart fileBodyPart = new MimeBodyPart();
    fileBodyPart.attachFile("/path-to/attach.txt");

    MimeBodyPart fileBodyPart2 = new MimeBodyPart();
    fileBodyPart2.attachFile("/path-to/attach2.txt");

    Multipart multipart = new MimeMultipart();
    multipart.addBodyPart(messageBodyPart);
    multipart.addBodyPart(fileBodyPart);
    //add another body part to supply another attachment
    multipart.addBodyPart(fileBodyPart2);
    message.setContent(multipart);
    Transport.send(message);
} catch (MessagingException | IOException e) {
    e.printStackTrace();
}

它是如何工作的

发送包含 HTML 内容的电子邮件与发送包含标准文本的电子邮件基本相同,唯一的区别是内容类型。当您在电子邮件的邮件正文部分设置内容时,您可以将内容设置为 text/html,以便将内容视为 html。有各种方法来构造 HTML 内容,包括使用链接、照片或任何其他有效的 HTML 标记。在这个例子中,一些基本的 HTML 标签被嵌入到一个字符串中。

虽然示例代码在实际系统中可能不是很有用,但是生成包含在电子邮件中的动态 HTML 内容是很容易的。最基本的形式是,动态生成的 HTML 可以是连接在一起形成 HTML 的文本字符串。

19-5.向一组收件人发送电子邮件

问题

您希望向多个收件人发送同一封电子邮件。

解决办法

使用 JavaMail API 中的 setRecipients()方法向多个收件人发送电子邮件。setRecipients()方法允许您一次指定多个收件人。例如:

// Main send body
    message.setFrom(new InternetAddress("someone@somewhere.com"));
    message.setRecipients(Message.RecipientType.TO, getRecipients(emails));
    message.setSubject("Subject");
    message.setContent("This is a test message", "text/plain");
    Transport.send(message);

// ------------------

    private Address[] getRecipients(List<String> emails) throws AddressException {
        Address[] addresses = new Address[emails.size()];
        for (int i =0;i < emails.size();i++) {
            addresses[i] = new InternetAddress(emails.get(i));
        }
        return addresses;
    }

它是如何工作的

通过使用 Message 对象的 setRecipients()方法,可以在同一封邮件上指定多个收件人。setRecipients()方法接受 Address 对象的数组。在这个配方中,因为您有一个字符串集合,所以您创建一个与集合大小相同的数组,并创建 InternetAddress 对象来填充该数组。使用多个电子邮件地址发送电子邮件(相对于单个电子邮件)要高效得多,因为只有一条消息从您的客户端发送到目标邮件服务器。然后,每个目标邮件服务器将向其拥有邮箱的所有收件人发送邮件。例如,如果您要向五个不同的 yahoo.com 帐户发送邮件,yahoo.com 邮件服务器只需接收邮件的一个副本,它就会将邮件发送给邮件中指定的所有 yahoo.com 收件人。

小费

如果要发送批量邮件,您可能需要将收件人类型指定为“密件抄送”,这样收到的电子邮件就不会显示收到该电子邮件的其他所有人。为此,请指定 Message。setRecipients()方法中的 RecipientType.BCC。

19-6.检查电子邮件

问题

您需要检查是否有新邮件到达指定的电子邮件帐户。

解决办法

您可以使用 javax.mail.Store 连接、查询和检索来自 Internet 邮件访问协议(IMAP)电子邮件帐户的邮件。例如,下面的代码连接到一个 IMAP 帐户,从该 IMAP 帐户中检索最后五封邮件,并将这些邮件标记为已读。

Session session = Session.getDefaultInstance(properties, null);
Store store = session.getStore("imaps");
    store.connect(host,username,password);
    System.out.println(store);
    Folder inbox = store.getFolder(folder);
    inbox.open(Folder.READ_WRITE);
    int messageCount = inbox.getMessageCount();
    int startMessage = messageCount - 5;
    int endMessage = messageCount;
    if (messageCount < 5) startMessage =0;
    Message messages[]  = inbox.getMessages(startMessage,endMessage);
for (Message message : messages) {
    boolean hasBeenRead = false;
    for (Flags.Flag flag :message.getFlags().getSystemFlags()) {
        if (flag == Flags.Flag.SEEN) {
            hasBeenRead = true;
            break;
        }
    }
    message.setFlag(Flags.Flag.SEEN, false);
    System.out.println(message.getSubject() + " "+ (hasBeenRead? "(read)" : "") + message.getContent());

}
inbox.close(true);

它是如何工作的

Store 对象允许您访问电子邮件邮箱信息。通过创建存储,然后请求收件箱文件夹,您可以访问 IMAP 帐户主邮箱中的邮件。使用文件夹对象,您可以请求从收件箱下载邮件。为此,可以使用 getMessages (start,end)方法。收件箱还提供了一个 getMessageCount()方法,该方法允许您知道收件箱中有多少电子邮件。请记住,消息从索引 1 开始。

每条消息都有一组标志,可以判断该消息是否已被阅读(标志。Flag.SEEN)或消息是否已被回复(Flags。Flag .已回答)。通过解析 SEEN 标志,您可以处理以前没有阅读过的消息。

若要将邮件设置为已读(或已回复),请调用 message.setFlag()方法。此方法允许您设置(或重置)电子邮件标志。如果要设置邮件标志,您需要以 READ_WRITE 方式打开文件夹,这样您就可以更改电子邮件标志。您还需要在代码末尾调用 inbox.close(true ),这将告诉 JavaMail API 将更改刷新到 IMAP 存储中。

小费

对于 SSL 上的 IMAP,应该使用 session.getStore("imaps ")。这就创建了一个安全的 IMAP 存储。

19-7.监控电子邮件帐户

问题

您希望监控电子邮件到达某个帐户的时间,并且希望根据邮件的内容对其进行处理。

解决办法

从配方 19-6 的实施开始。然后添加 IMAP 标志操作,为您的应用创建一个健壮的电子邮件监视器。在下面的示例中,checkForMail()方法用于处理发送到邮件列表的邮件。在这种情况下,用户可以通过在主题行中放置其中一个单词来订阅或取消订阅列表。以下示例检查新邮件的主题,并适当地处理它们。该示例还使用消息标志来删除已处理的消息,这样就不需要读取两次。无法处理的消息会被标记为已读,但会留在服务器中由人工进行故障排除。

private void checkForMail() {
        System.out.println("Checking for mail");
        Properties properties = new Properties();
        String username = "username";
        String password = "password";
        String folder = "Inbox";
        String host = "imap.server.com";

        try {
            Session session = Session.getDefaultInstance(properties, null);
            Store store = session.getStore("imaps");
            store.connect(host,username,password);
            Folder inbox = store.getFolder(folder);
            inbox.open(Folder.READ_WRITE);
            int messageCount = inbox.getMessageCount();
            Message messages[]  = inbox.getMessages(1,messageCount);
            for (Message message : messages) {
                boolean hasBeenRead = false;
                if (Arrays.asList(message.getFlags().getSystemFlags()).contains(Flags.Flag.SEEN)) {
                    continue;                     // not interested in "seen" messages
                }
                if (processMessage(message)) {
                    System.out.println("Processed :"+message.getSubject());
                    message.setFlag(Flags.Flag.DELETED, true);
                } else {
                    System.out.println("Couldn't Understand :"+message.getSubject());
                    // set it as seen, but keep it around
                    message.setFlag(Flags.Flag.SEEN, true);
                }
            }
            inbox.close(true);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }

    private boolean processMessage(Message message) throws MessagingException {
        boolean result = false;

        String subject = message.getSubject().toLowerCase();
        if (subject.startsWith("subscribe")) {
            String emailAddress = extractAddress (message.getFrom());
            if (emailAddress != null) {
                subscribeToList(emailAddress);
                result = true;
} 

        } else if (subject.startsWith("unsubscribe")) {
            String emailAddress = extractAddress (message.getFrom());
            if (emailAddress != null) {
                unSubscribeToList(emailAddress);
                result = true;
            }
        }

        return result;
    }

    private String extractAddress(Address[] addressArray) {
        if ((addressArray == null) || (addressArray.length < 1)) return null;
        if (!(addressArray[0] instanceof InternetAddress)) return null;
        InternetAddress internetAddress = (InternetAddress) addressArray[0];
        return internetAddress.getAddress();
    }

它是如何工作的

连接到 IMAP 服务器后,该示例请求收到的所有邮件。代码会跳过标记为可见的部分。为此,菜谱使用数组。将系统消息标志的数组转换成 ArrayList。一旦创建了列表,就需要查询列表以查看它是否包含该标志。看到枚举值。如果该值存在,则该示例跳到下一项。

当发现未读消息时,该消息由 processMessage()方法处理。该方法根据主题行的开头订阅或取消订阅消息的发送方。(这类似于邮件列表,发送主题为“subscribe”的消息会将发件人添加到邮件列表中。)

在确定执行哪个命令后,代码继续从消息中提取发件人的电子邮件。为此,processMessage()调用 extractEmail()方法。每封邮件都包含一组可能的“发件人”地址。这些地址对象是通用的,因为地址对象可以代表 Internet 或新闻组地址。在检查 Address 对象确实是 InternetAddress 后,代码将 Address 对象转换为 InternetAddress,并调用 getAddress()方法,该方法包含实际的电子邮件地址。

一旦提取出电子邮件地址,菜谱根据主题行调用 subscribe 或 unsubscribe。如果消息可以被理解(意味着消息已被处理),processMessage()方法返回 true(如果它不能理解消息,则返回 false)。在 checkForMail()方法中,当 processMessage()方法返回 true 时,邮件被标记为删除(通过调用 message.setFlag(Flags。Flag.DELETED,true);否则,该邮件将被标记为已查看。这使得消息在不被理解的情况下仍然存在,或者在被处理后被删除。最后,要提交邮件的新标志(并删除已删除的邮件),需要调用 inbox.close(true)方法。

19-8.摘要

电子邮件在我们今天使用的许多系统中扮演着重要的角色。Java 语言包括 JavaMail API,它使开发人员能够在其 Java 应用中包含健壮的电子邮件功能。本章中的方法涵盖了从安装到高级使用的 JavaMail API。要了解更多关于 JavaMail 以及与部署到企业应用服务器的 Java 应用的邮件集成的信息,请参考在线文档:www . Oracle . com/tech network/Java/JavaMail/index-141777 . html

二十、JSON 和 XML 处理

JSON 是最新的,也是最广泛使用的媒体形式之一,用于在两台或多台机器之间发送通信。在扩展形式中,它代表 JavaScript 对象符号。在 Java 9 的规划阶段,计划在发行版中包含一个标准的 JSON 处理(JSON-P) API,但是,增强建议并没有包含在发行版中。相反,通过简单地包含 JSON-P 库(目前包含在 Java EE 中),使用 JSON 数据仍然非常容易。即将发布的 JSON-P 的部分计划是为 Java SE 提供直接支持。

XML APIs 对 Java 开发人员总是可用的,通常作为第三方库提供,可以添加到运行时类路径中。从 Java 7 开始,Java API for XML Processing (JAXP)、Java API for XML Binding (JAXB)和 Java API for XML Web Services(JAX-WS)都包含在核心运行时库中。您将遇到的最基本的 XML 处理任务只涉及几个用例:编写和读取 XML 文档,验证这些文档,以及使用 JAXB 来帮助编组/解组 Java 对象。

本章提供了执行 XML 和 JSON-P 任务的方法。JSON-P 方法需要包含 JSON-P API,这可以通过向 maven 应用添加依赖项来实现。在本章中,您将学习如何创建 JSON,以及将它写入磁盘并执行解析。

注意

本章示例的源代码可以在 org.java9recipes.chapter20 包中找到。

20-1.编写 XML 文件

问题

您希望创建一个 XML 文档来存储应用数据。

解决办法

若要编写 XML 文档,请使用 javax . XML . stream . XML streamwriter 类。下面的代码循环访问 Patient 对象的数组,并将数据写入。xml 文件。这个示例代码来自 org . Java 9 recipes . chapter 20 . recipe 20 _ 1。DocWriter 示例:

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
...
public void run(String outputFile) throws FileNotFoundException, XMLStreamException,
        IOException {
    List<Patient> patients = new ArrayList<>();
    Patient p1 = new Patient();
    Patient p2 = new Patient();
    Patient p3 = new Patient();
    p1.setId(BigInteger.valueOf(1));
    p1.setName("John Smith");
    p1.setDiagnosis("Common Cold");
    p2.setId(BigInteger.valueOf(2));
    p2.setName("Jane Doe");
    p2.setDiagnosis("Broken Ankle");
    p3.setId(BigInteger.valueOf(3));
    p3.setName("Jack Brown");
    p3.setDiagnosis("Food Allergy");
    patients.add(p1);
    patients.add(p2);
    patients.add(p3);
    XMLOutputFactory factory = XMLOutputFactory.newFactory();
    try (FileOutputStream fos = new FileOutputStream(outputFile)) {
        XMLStreamWriter writer = factory.createXMLStreamWriter(fos, "UTF-8");
        writer.writeStartDocument();
        writer.writeCharacters("\n");
        writer.writeStartElement("patients");
        writer.writeCharacters("\n");
        for (Patient p : patients) {
            writer.writeCharacters("\t");
            writer.writeStartElement("patient");
            writer.writeAttribute("id", String.valueOf(p.getId()));
            writer.writeCharacters("\n\t\t");
            writer.writeStartElement("name");
            writer.writeCharacters(p.getName());
            writer.writeEndElement();
            writer.writeCharacters("\n\t\t");
            writer.writeStartElement("diagnosis");
            writer.writeCharacters(p.getDiagnosis());
            writer.writeEndElement();
            writer.writeCharacters("\n\t");
            writer.writeEndElement();
            writer.writeCharacters("\n");
        }
        writer.writeEndElement();
        writer.writeEndDocument();
        writer.close();
    }

}

前面的代码写入了以下文件内容:

<?xml version="1.0" ?>
<patients>
    <patient id="1">
        <name>John Smith</name>
        <diagnosis>Common Cold</diagnosis>
    </patient>
    <patient id="2">
        <name>Jane Doe</name>
        <diagnosis>Broken ankle</diagnosis>
    </patient>
    <patient id="3">
        <name>Jack Brown</name>
<diagnosis>Food allergy</diagnosis>
</patient>
</patients>

它是如何工作的

Java 标准库提供了几种编写 XML 文档的方法。一个模型是 XML 的简单 API(SAX)。更新、更简单、更高效的模型是 XML 流 API(StAX)。这个菜谱使用 javax.xml.stream 包中定义的 StAX。编写 XML 文档需要五个步骤:

  1. 创建文件输出流。

  2. 创建 XML 输出工厂和 XML 输出流编写器。

  3. 在 XML 流编写器中包装文件流。

  4. 使用 XML 流编写器的写入方法创建文档并写入 XML 元素。

  5. 关闭输出流。

使用 java.io.FileOutputStream 类创建文件输出流。您可以使用 try-block 来打开和关闭该流。在第九章中了解更多关于新 try-block 语法的信息。

javax . XML . stream . xmloutputfactory 提供了一个创建输出工厂的静态方法。使用工厂创建 javax . XML . stream . XML streamwriter。

一旦有了编写器,就将文件流对象包装在 XML 编写器实例中。您将使用各种写方法来创建 XML 文档元素和属性。最后,当您完成写入文件时,只需关闭 writer。XMLStreamWriter 实例的一些更有用的方法如下:

  • writeStartDocument()

  • writestartelemont_)

  • writeendelemont_)

  • writeEndDocument()

  • writesttribute _)

创建文件和 XMLStreamWriter 后,应该总是通过调用 writeStartDocumentMethod()方法来开始文档。接下来,通过组合使用 writeStartElement()和 writeEndElement()方法来编写单个元素。当然,元素可以有嵌套元素。您有责任按正确的顺序调用这些方法来创建格式良好的文档。使用 writeAttribute()方法将属性名称和值放入当前元素中。您应该在调用 writeStartElement()方法后立即调用 writeAttribute()。最后,用 writeEndDocument()方法通知文档结束,并关闭 Writer 实例。

使用 XMLStreamWriter 的一个有趣之处是它不格式化文档输出。除非您专门使用 writeCharacters()方法来输出空格和换行符,否则输出将流至单个无格式行。当然,这不会使生成的 XML 文件无效,但是它确实给人们阅读带来了不便和困难。因此,您应该考虑使用 writeCharacters()方法根据需要输出空格和换行符,以创建人类可读的文档。如果不需要文档具有可读性,可以安全地忽略这种编写额外空白和换行符的方法。不管格式如何,XML 文档都是格式良好的,因为它符合正确的 XML 语法。

该示例代码的命令行使用模式如下:

java org.java9recipes.chapter20.recipe20_1.DocWriter <outputXmlFile>

调用此应用以如下方式创建名为 patients.xml 的文件:

java org.java9recipes.chapter20.recipe20_1.DocWriter patients.xml

20-2.读取 XML 文件

问题

您需要解析 XML 文档,检索已知的元素和属性。

解决方案 1

使用 javax . XML . stream . XML streamreader 接口读取文档。使用这个 API,您的代码将使用类似于 SQL 中的类似光标的接口提取 XML 元素,依次处理每个元素。org.java9recipes.DocReader 中的以下代码片段演示了如何读取在前面的配方中生成的 patients.xml 文件:

public void cursorReader(String xmlFile)
throws FileNotFoundException, IOException, XMLStreamException {
    XMLInputFactory factory = XMLInputFactory.newFactory();
    try (FileInputStream fis = new FileInputStream(xmlFile)) {
        XMLStreamReader reader = factory.createXMLStreamReader(fis);
        boolean inName = false;
        boolean inDiagnosis = false;
        String id = null;
        String name = null;
        String diagnosis = null;

        while (reader.hasNext()) {
            int event = reader.next();
            switch (event) {
                case XMLStreamConstants.START_ELEMENT:
                    String elementName = reader.getLocalName();
                    switch (elementName) {
                        case "patient":
                            id = reader.getAttributeValue(0);
                            break;
                        case "name":
                            inName = true;
                            break;
                        case "diagnosis":
                            inDiagnosis = true;
                            break;
                        default:
                            break;
                    }
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    String elementname = reader.getLocalName();
                    if (elementname.equals("patient")) {
                        System.out.printf("Patient: %s\nName: %s\nDiagnosis: %s\n\n",id, name,
                        diagnosis);
                        id = name = diagnosis = null;
                        inName = inDiagnosis = false;
                    }
                    break;
                case XMLStreamConstants.CHARACTERS:
                    if (inName) {
                        name = reader.getText();
                        inName = false;
                    } else if (inDiagnosis) {
                        diagnosis = reader.getText();
                        inDiagnosis = false;
                    }
                    break;
                default:
                    break;
            }
        }
        reader.close();
    }
}

解决方案 2

使用 XMLEventReader 通过面向事件的接口读取和处理事件。这个 API 也被称为面向迭代器的 API。以下代码与解决方案 1 中的代码非常相似,只是它使用了面向事件的 API,而不是面向光标的 API。这个代码片段可以从同一个 org . Java 9 recipes . chapter 20 . recipe 20 _ 1 获得。解决方案 1 中使用的 DocReader 类:

public void eventReader(String xmlFile)
        throws FileNotFoundException, IOException, XMLStreamException {
    XMLInputFactory factory = XMLInputFactory.newFactory();
    XMLEventReader reader = null;
    try(FileInputStream fis = new FileInputStream(xmlFile)) {
        reader = factory.createXMLEventReader(fis);
        boolean inName = false;
        boolean inDiagnosis = false;
        String id = null;
        String name = null;
        String diagnosis = null;

        while(reader.hasNext()) {
            XMLEvent event = reader.nextEvent();
            String elementName = null;
            switch(event.getEventType()) {
                case XMLEvent.START_ELEMENT:
                    StartElement startElement = event.asStartElement();
                    elementName = startElement.getName().getLocalPart();
                    switch(elementName) {
                        case "patient":
                            id = startElement.getAttributeByName(QName.valueOf("id")).getValue();
                            break;
                        case "name":
                            inName = true;
                            break;
                        case "diagnosis":
                            inDiagnosis = true;
                            break;
                        default:
                            break;
                    }
                    break;
                case XMLEvent.END_ELEMENT:
                    EndElement endElement = event.asEndElement();
                    elementName = endElement.getName().getLocalPart();
                    if (elementName.equals("patient")) {
                        System.out.printf("Patient: %s\nName: %s\nDiagnosis: %s\n\n",id, name, diagnosis);
                        id = name = diagnosis = null;
                        inName = inDiagnosis = false;
                    }
                    break;
                case XMLEvent.CHARACTERS:
                    String value = event.asCharacters().getData();
                    if (inName) {
                        name = value;
                        inName = false;
                    } else if (inDiagnosis) {
                        diagnosis = value;
                        inDiagnosis = false;
                    }
                    break;
            }
        }
    }
    if(reader != null) {
        reader.close();
    }
}

它是如何工作的

Java 提供了几种读取 XML 文档的方法。一种方法是使用 StAX,一种流模型。它比旧的 SAX API 更好,因为它允许您读写 XML 文档。尽管 StAX 不如 DOM API 强大,但它是一个优秀而高效的 API,对内存资源的消耗较少。

StAX 提供了两种读取 XML 文档的方法:游标 API 和迭代器 API。面向游标的 API 利用游标从头到尾遍历 XML 文档,一次指向一个元素,并且总是向前移动。迭代器 API 将 XML 文档流表示为一组离散的事件对象,按照它们在源 XML 中的读取顺序提供。此时,面向事件的迭代器 API 优于游标 API,因为它为 XMLEvent 对象提供了以下好处:

  • XMLEvent 对象是不可变的,即使 StAX 解析器已经转移到后续事件,这些对象也可以保持不变。您可以将这些 XMLEvent 对象传递给其他进程,或者将它们存储在列表、数组和映射中。

  • 您可以子类化 XMLEvent,根据需要创建您自己的专用事件。

  • 您可以通过添加或移除事件来修改传入的事件流,这比游标 API 更灵活。

要使用 StAX 读取文档,请在文件输入流上创建一个 XML 事件读取器。使用 hasNext()方法检查事件是否仍然可用,并使用 nextEvent()方法读取每个事件。nextEvent()方法将返回特定类型的 XMLEvent,它对应于 XML 文件中的开始和停止元素、属性和值数据。当您使用完这些对象时,记得关闭您的阅读器和文件流。

您可以像这样调用示例应用,使用 patients.xml 文件作为您的参数:

java org.java9recipes.chapter20.recipe20_2.DocReader <xmlFile>

20-3.转换 XML

问题

您希望将 XML 文档转换为另一种格式,例如 HTML。

解决办法

使用 javax.xml.transform 包将 xml 文档转换为另一种文档格式。

下面的代码演示如何读取源文档,应用可扩展样式表语言(XSL)转换文件,并生成转换后的新文档。使用 org . Java 9 recipes . chapter 20 . recipe 20 _ 3 中的示例代码。TransformXml 类读取 patients.xml 文件并创建 patients.xml 文件。下面的代码片段展示了这个类的重要部分:

import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
...
public void run(String xmlFile, String xslFile, String outputFile)
        throws FileNotFoundException, TransformerConfigurationException, TransformerException {
    InputStream xslInputStream = new FileInputStream(xslFile);
    Source xslSource = new StreamSource(xslInputStream);
    TransformerFactory factory = TransformerFactory.newInstance();
    Transformer transformer = factory.newTransformer(xslSource);
    InputStream xmlInputStream = new FileInputStream(xmlFile);
    StreamSource in = new StreamSource(xmlInputStream);
    StreamResult out = new StreamResult(outputFile);
    transformer.transform(in, out);    
    ...
}

它是如何工作的

javax.xml.transform 包包含将 xml 文档转换成任何其他文档类型所需的所有类。最常见的用例是将面向数据的 XML 文档转换成用户可读的 HTML 文档。

从一种文档类型转换到另一种文档类型需要三个文件:

  • XML 源文档

  • 将 XML 元素映射到新文档元素的 XSL 转换文档

  • 目标输出文件

XML 源文档当然是您的源数据文件。它通常包含易于编程解析的面向数据的内容。然而,人们不容易阅读 XML 文件,尤其是复杂的、数据丰富的文件。相反,人们更愿意阅读正确呈现的 HTML 文档。

XSL 转换文档指定如何将 XML 文档转换成不同的格式。XSL 文件通常包含一个 HTML 模板,该模板指定了动态字段,这些字段将保存源 XML 文件的提取内容。

在这个例子的源代码中,您会发现两个源文档:

  • 第二十章/recipe20_3/patients.xml

  • 第二十章/收件人 20_3/patients.xsl

patients.xml 文件很短,包含以下数据:

<?xml version="1.0" encoding="UTF-8"?>
<patients>
    <patient id="1">
        <name>John Smith</name>
        <diagnosis>Common Cold</diagnosis>
    </patient>
    <patient id="2">
        <name>Jane Doe</name>
        <diagnosis>Broken ankle</diagnosis>
    </patient>
    <patient id="3">
        <name>Jack Brown</name>
        <diagnosis>Food allergy</diagnosis>
    </patient>
</patients>

patients.xml 文件定义了名为 patients 的根元素。它有三个嵌套的病人元素。患者元素包含三段数据:

  • 患者标识符,作为患者元素的 id 属性提供

  • 患者姓名,作为姓名子元素提供

  • 患者诊断,作为诊断子元素提供

转换 xsl 文档(patients.xsl)也很小,它只是使用 XSL 将患者数据映射为更易于用户阅读的 HTML 格式:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:template match="/">
<html>
<head>
    <title>Patients</title>
</head>
<body>
    <table border="1">
        <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Diagnosis</th>
        </tr>
        <xsl:for-each select="patients/patient">
        <tr>
            <td>
        <xsl:value-of select="@id"/>
            </td>
            <td>
        <xsl:value-of select="name"/>
            </td>
            <td>
        <xsl:value-of select="diagnosis"/>
            </td>
            </tr>
        </xsl:for-each>
    </table>
</body>
</html>
        </xsl:template>
        </xsl:stylesheet>

使用这个样式表,示例代码将 XML 转换成包含所有患者及其数据的 HTML 表。在浏览器中呈现时,HTML 表格应该如图 20-1 所示。

A323910_3_En_20_Fig1_HTML.jpg

图 20-1。HTML 表格的常见呈现

使用这个 XSL 文件将 XML 转换成 HTML 文件的过程很简单,但是每一步都可以通过额外的错误检查和处理来增强。对于此示例,请参考解决方案一节中前面的代码。

最基本的转换步骤如下:

  1. 将 XSL 文档作为源对象读入 Java 应用。

  2. 创建一个 Transformer 实例,并提供您的 XSL 源实例供它在操作过程中使用。

  3. 创建表示源 XML 内容的 SourceStream。

  4. 为输出文档创建一个 StreamResult 实例,在本例中是一个 HTML 文件。

  5. 使用 Transformer 对象的 transform()方法来执行转换。

  6. 根据需要关闭所有相关的流和文件实例。

如果您选择执行示例代码,您应该以下列方式调用它,使用 patients.xml、patients.xsl 和 patients.xml 作为参数:

java org.java9recipes.chapter20.recipe20_3.TransformXml <xmlFile><xslFile><outputFile>

20-4.验证 XML

问题

您希望确认您的 XML 是有效的——它符合已知的文档定义或模式。

解决办法

使用 javax.xml.validation 包验证您的 XML 是否符合特定的模式。以下代码片段来自 org . Java 9 recipes . chapter 20 . recipe 20 _ 4。ValidateXml 演示了如何根据 Xml 模式文件进行验证:

import java.io.File;
import java.io.IOException;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.xml.sax.SAXException;
...
public void run(String xmlFile, String validationFile) {
    boolean valid = true;
    SchemaFactory sFactory =
            SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    try {
        Schema schema = sFactory.newSchema(new File(validationFile));
        Validator validator = schema.newValidator();
        Source source = new StreamSource(new File(xmlFile));
        validator.validate(source);
    } catch (SAXException | IOException | IllegalArgumentException ex) {
        valid = false;
    }
    System.out.printf("XML file is %s.\n", valid ? "valid" : "invalid");
}
...

它是如何工作的

使用 XML 时,验证它以确保语法正确,并确保 XML 文档是指定 XML 模式的实例是很重要的。验证过程包括比较模式和 XML 文档,找出任何差异。javax.xml.validation 包提供了根据各种模式可靠地验证 xml 文件所需的所有类。将用于 XML 验证的最常见模式被定义为 XMLConstants 类中的常量 URIs:

  • XMLConstants。W3C _ XML _ 架构 _NS_URI

  • XML 常量。松弛 _NS_URI

首先为特定类型的模式定义创建一个 SchemaFactory。SchemaFactory 知道如何解析特定的模式类型,并为验证做准备。使用 SchemaFactory 实例创建架构对象。模式对象是模式定义语法的内存表示。您可以使用 Schema 实例来检索理解这种语法的验证器实例。最后,使用 validate()方法检查 XML。如果在验证过程中出现任何问题,方法调用将生成几个异常。否则,validate()方法会安静地返回,您可以继续使用 XML 文件。

注意

XML 模式在 2001 年第一次获得万维网联盟(W3C)的“推荐”地位。竞争模式从此变得可用。一个竞争模式是 XML 下一代正则语言(RELAX NG)模式。RELAX NG 可能是一种更简单的模式,它的规范也定义了一种非 XML 的紧凑语法。这个食谱的例子使用了 XML 模式。

使用以下命令行语法运行示例代码,最好使用示例。xml 文件和验证文件分别作为 resources/patients.xml 和 patients.xsl 提供:

java org.java9recipes.chapter20.recipe20_4.ValidateXml <xmlFile><validationFile>

20-5.为 XML 模式创建 Java 绑定

问题

您希望生成一组 Java 类(Java 绑定),它们代表 XML 模式中的对象。

解决办法

JDK 提供了一个工具,可以将模式文档转换成有代表性的 Java 类文件。使用 <jdk_home>/bin/xjc 命令行工具为 XML 模式生成 Java 绑定。要从配方 20-3 中为 patients.xsd 文件创建 Java 类,您可以在控制台中发出以下命令:</jdk_home>

xjc –p org.java9recipes.chapter20.recipe20_5 patients.xsd

该命令将处理 patients.xsd 文件,并创建处理用该模式验证的 XML 文件所需的所有类。对于此示例,patients.xsd 文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="patients">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" name="patient" type="Patient"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="Patient">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="diagnosis" type="xs:string"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" use="required"/>
</xs:complexType>
</xs:schema>

在前面的 xsd 文件上执行的 xjc 命令在 org . Java 9 recipes . chapter 20 . recipe 20 _ 5 包中创建以下文件:

  • ObjectFactory.java

  • Patients.java

  • Patient.java

它是如何工作的

JDK 包括了 <jdk_home>/bin/xjc 实用程序。xjc 实用程序是一个命令行应用,它从模式文件创建 Java 绑定。源模式文件可以有多种类型,包括 XML 模式、RELAX NG 等。</jdk_home>

xjc 命令有几个选项来执行它的工作。一些最常见的选项指定了源模式文件、生成的 Java 绑定文件的包以及将接收 Java 绑定文件的输出目录。

您可以通过使用工具的–help 选项获得所有命令行选项的详细描述:

xjc –help

Java 绑定包含带注释的字段,这些字段对应于 XML 模式文件中定义的字段。这些注释标记了模式文件的根元素和所有其他子元素。这在 XML 处理的下一步中非常有用,包括解组或编组这些绑定。

20-6.将 XML 解组到 Java 对象

问题

您希望解组一个 XML 文件,并创建其对应的 Java 对象树。

解决办法

解组是将数据格式(在本例中为 XML)转换成对象的内存表示形式以便用于执行任务的过程。JAXB 提供了一个解组服务,它解析一个 XML 文件,并根据您在 Recipe 20-4 中创建的绑定生成 Java 对象。以下代码可以从 org . Java 9 recipes . chapter 20 . recipe 20-6 包中读取 patients.xml 文件,以创建 patients 根对象及其 Patient 对象列表:

public void run(String xmlFile, String context)
        throws JAXBException, FileNotFoundException {
    JAXBContext jc = JAXBContext.newInstance(context);
    Unmarshaller u = jc.createUnmarshaller();
    FileInputStream fis = new FileInputStream(xmlFile);
    Patients patients = (Patients)u.unmarshal(fis);
    for (Patient p: patients.getPatient()) {
        System.out.printf("ID: %s\n", p.getId());
        System.out.printf("NAME: %s\n", p.getName());
        System.out.printf("DIAGNOSIS: %s\n\n", p.getDiagnosis());
    }
}

如果您在 chapter 20/recipe 20 _ 6/patients . XML 文件上运行示例代码并使用 org.java9recipes.chapter20 上下文,应用将在遍历患者对象列表时向控制台打印以下内容:

ID: 1
NAME: John Smith
DIAGNOSIS: Common Cold

ID: 2
NAME: Jane Doe
DIAGNOSIS: Broken ankle

ID: 3
NAME: Jack Brown
DIAGNOSIS: Food allergy
注意

前面的输出直接来自 Java Patient 类的实例,该类是由 XML 表示创建的。代码不直接打印 XML 文件的内容。相反,在 XML 被整理成适当的 Java 绑定实例之后,它打印 Java 绑定的内容。

它是如何工作的

将 XML 文件解组为 Java 对象表示至少有两个标准:

  • 一个格式良好且有效的 XML 文件

  • 一组相应的 Java 绑定

Java 绑定不必通过 xjc 命令自动生成。一旦您获得了一些 Java 绑定和注释特性的经验,您可能更喜欢通过手工制作 Java 绑定来创建和控制 Java 绑定的所有方面。无论您的偏好是什么,Java 的解组服务都利用绑定及其注释将 XML 对象映射到目标 Java 对象,并将 XML 元素映射到目标对象字段。

使用以下语法执行该配方的示例应用,用 patients.xml 和 org . Java 9 recipes . chapter 20 . recipe 20 _ 6 替换相应的参数:

java org.java9recipes.chapter20.recipe20_6.UnmarshalPatients <xmlfile><context>

20-7.用 JAXB 构建 XML 文档

问题

您需要将对象的数据写入 XML 表示。

解决办法

假设您已经按照 Recipe 20-4 中的描述为 XML 模式创建了 Java 绑定文件,那么您可以使用 JAXBContext 实例来创建一个编组器对象。然后使用 Marshaller 对象将 Java 对象树序列化为 XML 文档。下面的代码演示了这一点:

public void run(String xmlFile, String context)
        throws JAXBException, FileNotFoundException {
    Patients patients = new Patients();
    List<Patient> patientList = patients.getPatient();
    Patient p = new Patient();
    p.setId(BigInteger.valueOf(1));
    p.setName("John Doe");
    p.setDiagnosis("Schizophrenia");
    patientList.add(p);

    JAXBContext jc = JAXBContext.newInstance(context);
    Marshaller m = jc.createMarshaller();
    m.marshal(patients, new FileOutputStream(xmlFile));
}

前面的代码生成了一个无格式但格式良好的有效 XML 文档。为了提高可读性,XML 文档的格式如下:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <patients>
    <patient id="1">
        <name>John Doe</name>
        <diagnosis>Schizophrenia</diagnosis>
    </patient>
    </patients>
注意

前面代码中的 getPatient()方法返回患者对象的列表,而不是单个患者。在这个例子中,这是从 XSD 模式生成的 JAXB 代码的一个奇怪的命名。

它是如何工作的

编组器对象理解 JAXB 注释。在处理类时,它使用 JAXB 注释来提供用 XML 创建对象树所需的上下文。

您可以从 org . Java 9 recipes . chapter 20 . recipe 20 _ 7 运行前面的代码。使用以下命令行封送患者应用:

java org.java9recipes.chapter20.recipe20_7.MarshalPatients <xmlfile><context>

上下文参数指的是您将封送的 Java 类的包。在前面的示例中,因为代码封送了一个 Patients 对象树,所以正确的上下文是 Patients 类的包名。在本例中,上下文是 org.java9recipes.chapter20。

20-8.解析 XML 目录

问题

出于安全目的或其他需要,您需要解析 XML 目录,以便将远程外部引用指向本地目录。

解决办法

利用 Java 9 中的标准 XML 目录 API。在本例中,使用 API 读取并解析本地目录。

public static void main(String[] args) {
        // Create a CatalogFeatures object
        CatalogFeatures defaults = CatalogFeatures.defaults();

        // Resolve using properties
        // System.setProperty("javax.xml.catalog.files", "catalog.xml");

        // Resolve by passing
        Catalog catalog = CatalogManager.catalog(defaults, "catalog.xml", "catalog-alt.xml");

        // Use CatalogFeatures to specify catalog files and/or additional features
        // CatalogFeatures catalogFeatures = CatalogFeatures.builder()
        //         .with(Feature.FILES, "catalog.xml")
        //         .with(Feature.RESOLVE, "ignore")
        //         .build();

        // Stream and filter to find the catalog matching your specification
        Optional<Catalog> cat = catalog.catalogs()
                .filter((c)->c.matchURI("calstblx.dtd") != null)
                .findFirst();

        // Do something with catalog
    }

它是如何工作的

JDK 历来将 XML 解析器作为其核心的一部分。然而,这个解析器是私有的,仅由 JDK 使用。随着时间的推移,实现公共 XML 解析器的需求变得越来越明显,因此私有解析器被改进为一个新的公共 API。API 允许管理 XML 目录和解析器的创建,它实现了 OASIS XML 目录 1.1 规范,并且实现了现有的 JAXP 接口。

有许多关键的接口和类组成了 Catalog API。目录接口可用于表示实体目录。CatalogManager 用于通过传递 CatalogFeatures 配置对象以及包含 XML 目录文件路径的变量参数来解析目录。它还可以用于生成 CatalogResolvers。还可以通过指定“javax.xml.catalog.files”属性来传递一个或多个目录文件的路径,如示例所示。

System.setProperty("javax.xml.catalog.files", "catalog.xml");

CatalogFeatures 对象包含许多属性和功能,调用 CatalogFeatures.defaults()方法可以获得默认实现。要为 CatalogFeatures 对象指定不同的值,可以利用构建器模式来指示每个不同特性的值。这些特征可以在表 20-1 中看到。

表 20-1。目录功能
|

特征

|

财产

|

描述

|
| --- | --- | --- |
| 文件 | javax.xml.catalog.files | 分号分隔的目录文件列表。 |
| 更喜欢 | javax.xml.catalog.prefer | 指示公共标识符和系统标识符之间的首选项。 |
| 推迟 | javax.xml.catalog.defer | 指示只有在需要时才会读取委托目录。 |
| 分解 | javax.xml.catalog.resolve | 确定未找到匹配目录时要采取的操作。 |

有关 CatalogFeatures 的更多信息,请参考 JavaDoc(download . Java . net/Java/JDK 9/docs/API/javax/XML/catalog/catalog features . html)。

可以调用 Catalog.catalogs()方法,使用当前目录中的 nextCatalog 条目生成一个备选目录流。这种解析可以用来匹配 XML 目录中的条目。

XML Catalog API 是对 JDK 的一个很好的补充,使得在需要时利用本地目录而不是远程目录变得容易。Java 很早就有了目录解析器,但是它不能在 JDK 内部之外使用。新的 API 是旧的私有 API 的更新形式,它完全符合 OASIS XML Catalogs 1.1 规范。

20-9.使用 JSON

问题

您对在 Java SE 9 应用中使用 JSON 感兴趣。

解决办法

将 JSON-P API 作为依赖项添加到 Java SE 9 应用中。有几个选项可以添加依赖项。可以下载 JAR 并将其放入类路径,或者如果使用 Maven 之类的构建工具,只需添加项目存储库的坐标。下面几行摘自 POM 文件(Maven 的项目对象模块),说明如何添加依赖项。

<dependencies>
        <dependency>
            <groupId>javax.json</groupId>
            <artifactId>javax.json-api</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.0.4</version>
        </dependency>
...
</dependencies>

它是如何工作的

随着 Java EE 7 的发布,JavaScript Object Notation(JSON-P)API 被添加到 Java 企业平台中。JSON-P,也称为“JSON 处理”,已经成为使用 Java 构建 JSON 对象的标准方式。因为 Java 9 没有捆绑 JSON 构建和解析 API,所以必须引入必要的依赖项来利用标准化的 JSON-P API。JSON-P 是 Java EE 的一部分,但是在这一点上 Java SE 没有提供支持。因此,通过将下载的 JAR 文件添加到类路径中,或者将 Maven 坐标添加到项目 POM 文件中,很容易包含 API。在解决方案中,我介绍了如何利用 Maven 坐标。但是,请确保相应地更新版本。

20-10.构建 JSON 对象

问题

您希望在 Java 应用中构建一个 JSON 对象。

解决办法

利用 JSON-P API 构建一个 JSON 对象。在下面的代码中,构建了一个属于一本书的 JSON 对象。

public JsonObject buildBookObject() {
    JsonBuilderFactory factory = Json.createBuilderFactory(null);
    JsonObject obj = factory.createObjectBuilder()
            .add("title", "Java 9 Recipes")
            .add("author", "Josh Juneau")
            .add("projectCoordinator", "Jill Balzano")
            .add("editor", "Jonathan Gennick")
            .build();

    return obj;
}

它是如何工作的

JSON-P API 包括一个 helper 类,可以使用 builder 模式创建 JSON 对象。使用 JsonObjectBuilder,可以通过一系列方法调用构建 JSON 对象,每个方法调用都建立在另一个方法调用的基础上——因此,就有了构建器模式。一旦构建了 JSON 对象,就可以调用 JsonObjectBuilder.build()方法来返回 JSON object。

在这个菜谱的例子中,您构建了一个 JSON 对象,它提供了关于一本书的详细信息。JsonObjectBuilder.beginObject()方法用于表示正在创建一个新对象。add 方法用于添加更多的名称/值属性,非常类似于映射。因此,下面一行添加了一个名为 title 的属性,其值为“Java 9 Recipes”:

.add("title", "Java 9 Recipes")

对象可以相互嵌入,在一个 JsonObject 中创建子部分的层次结构。例如,在第一次调用 add()之后,通过调用 jsonbuilderfactory . createobjectbuilder()作为 add()操作的值,并传递嵌入对象的名称,可以将另一个对象嵌入到初始 JsonObject 中。嵌入对象也可以包含属性;因此,要向嵌入对象添加属性,请在嵌入对象中调用 add()方法。JsonObjects 可以根据需要包含任意多的嵌入对象。如果我们要修改示例中的源代码,按名字和姓氏分解作者,下面几行代码演示了嵌入对象定义的开始和结束:

.add("author", factory.createObjectBuilder()
    .add("first", "Josh")
    .add("last", "Juneau"))
.add("projectCoordinator", "Jill Balzano")

JsonObject 也可能有一个相关子对象的数组。若要添加子对象的数组,请调用 jsonbuilderfactory . createarraybuilder()方法,并将数组的名称作为参数传递。数组可以由对象组成,甚至可以由对象、数组等的层次结构组成。

一旦创建了 JsonObject,就可以将其传递给客户机。WebSockets 可以很好地将 JsonObjects 传递回客户机,但是有许多不同的技术可以用来与 JSON 通信。

20-11.将 JSON 对象写入文件

问题

您已经生成或解析了一个 JSON 对象,并且希望将它以文件格式存储在磁盘上。

解决办法

利用 JSON-P API 构建一个 JSON 对象,然后将它存储到文件系统中。JsonWriter 类可以在磁盘上创建一个文件,然后将 JSON 写入该文件。在下面的例子中,在配方 20-10 中生成的 JsonObject 使用这种技术写入磁盘。

public static void writeJson() {
    JsonObject jsonObject = buildBookObject();
    try (javax.json.JsonWriter jsonWriter = Json.createWriter(new FileWriter("Book.json"))) {
        jsonWriter.writeObject(jsonObject);
    } catch (IOException ex) {
        System.out.println(ex);
    }
}

它是如何工作的

JsonWriter 类可用于将 JsonObject 写入 Java writer 对象。通过将 Writer 对象作为参数传递给 Json.createWriter()方法来实例化 JsonWriter。创建 JsonWriter 后,可以调用 JsonWriter.writeObject()方法,传递要编写的 JsonObject。一旦编写了 JsonObject,就可以通过调用它的 close()方法来关闭 JsonWriter。这些是将 JSON 对象写入 Java Writer 类类型所必需的唯一步骤。

20-12.解析 JSON 对象

问题

您创建的应用需要能够读取 JSON 对象并相应地解析它。

解决办法

利用 JsonReader 对象读取 JSON 对象,然后利用 JsonParser 对象对 JSON 数据执行操作。下面的例子演示了如何从磁盘读取一个文件,然后解析它以显示一些内容。

public void parseObject() {
    Reader fileReader = new InputStreamReader(getClass().getResourceAsStream("Book.json"));
    JsonParser parser = Json.createParser(fileReader);
    while (parser.hasNext()) {
        Event ev = parser.next();
        System.out.println(ev);
        if (ev.equals(Event.VALUE_STRING)) {
            System.out.println(parser.getString());
        }
    }
}

在这个例子中,名为 Book.json 的 Json 文件被读取和解析。当解析过程中遇到 VALUE_STRING 事件时,将打印该字符串。还会打印每个遇到的事件。以下输出是结果:

START_OBJECT
KEY_NAME
VALUE_STRING
Java 9 Recipes
KEY_NAME
VALUE_STRING
Josh Juneau
KEY_NAME
VALUE_STRING
Jill Balzano
KEY_NAME
VALUE_STRING
Jonathan Gennick
END_OBJECT

它是如何工作的

一旦 JSON 对象被持久化到磁盘上,以后就需要将它读回以供使用。JsonReader 对象负责这项任务。要创建 JsonReader 对象,请调用 Json.createReader()方法,传递 InputStream 或 Reader 对象。一旦创建了 JsonReader 对象,就可以通过调用它的 readObject 方法来生成 JsonObject。

为了执行某些任务,必须对 JSON 对象进行解析,以便只找到对当前任务有用的内容。利用 JSON 解析器可以使这样的工作变得更容易,因为解析器能够将对象分解成多个部分,以便可以根据需要检查每个不同的部分,从而产生想要的结果。

javax.json.Json 类包含一个静态工厂方法 createParser(),该方法接受一组输入并返回一个可迭代的 JsonParser。表 20-2 列出了通过 createParser()方法接受的不同可能的输入类型。

表 20-2。createParser 方法输入类型
|

输入类型

|

方法调用

|
| --- | --- |
| 输入流 | createpresser(input stream in) |
| JsonArray | createpresser(jsonaarray arr) |
| JsonObject | createParser(JsonObject obj) |
| 读者 | createParser(阅读器阅读器) |

一旦创建了 JsonParser,就可以将它变成事件对象的迭代器。每个事件都与 JSON 对象中的不同结构相关联。例如,当创建 JSON 对象时,会发生 START_OBJECT 事件,添加名称/值对会触发 KEY_NAME 和 VALUE_STRING 事件。可以利用这些事件从 JSON 对象中获取所需的信息。在本例中,事件名称只是打印到服务器日志中。然而,在现实生活的应用中,条件最有可能测试每个迭代,以找到一个特定的事件,然后执行一些处理。表 20-3 列出了不同的 JSON 事件,以及每个事件发生的时间描述。

表 20-3。JSON 对象事件
|

事件

|

出现

|
| --- | --- |
| 开始 _ 对象 | 对象的开始。 |
| 结束对象 | 对象的结尾。 |
| 开始 _ 数组 | 数组的开始。 |
| END _ 数组 | 数组结尾。 |
| KEY_NAME | 密钥的名称。 |
| 值 _ 字符串 | 字符串格式的名称/值对的值。 |
| 值 _ 数字 | 数值格式的名称/值对的值。 |
| 值 _ 真 | 布尔格式的名称/值对的值。 |
| VALUE_FALSE | 布尔格式的名称/值对的值。 |
| 值为空 | 名称/值对的值为 NULL。 |

摘要

XML 通常用于在不同的应用之间传输数据,或者将某种类型的数据存储到文件中。因此,理解在应用开发平台中使用 XML 的基础非常重要。本章概述了如何使用 Java 执行一些处理 XML 的关键任务。本章从编写和阅读 XML 的基础开始。然后演示了如何将 XML 转换成不同的格式,以及如何根据 XML 模式进行验证。

这一章还提到了使用 JSON。尽管 Java SE 9 没有附带 JSON API,但是可以很容易地利用 JSON-P API 来生成、编写和解析 JSON 数据。本章演示了如何执行这些任务。

二十一、网络

今天,编写一个不以某种方式在互联网上通信的应用已经很少见了。从向另一台机器发送数据,到从远程网页上抓取信息,网络在当今的计算世界中扮演着不可或缺的角色。Java 使用新的 I/O (NIO)和 Java 平台(NIO . 2)API 的更多新的 I/O 特性,使得通过网络进行通信变得容易。Java SE 7 包含了一些新特性,使得多播变得更加容易。随着这些新特性的加入,Java 平台包含了大量的编程接口来帮助完成网络任务。Java 9 引入了新的 HTTP/2 客户机,它提供了一个简单明了的 API,并对旧的 HTTP/1.1 客户机进行了性能改进。

本章并不试图涵盖 Java 语言中的每一个网络特性,因为这个主题相当大。然而,它确实提供了一些对广大开发人员最有用的方法。您了解了一些标准的网络概念,比如套接字,以及 Java 语言最新版本中引入的一些新概念。如果你觉得这一章很有趣,并且想学习更多关于 Java 网络的知识,你可以在网上找到很多资源。要了解更多信息,最好的地方可能是位于download . Oracle . com/javase/tutorial/networking/index . html的 Oracle 文档。

21-1.监听服务器上的连接

问题

您希望创建一个服务器应用来侦听来自远程客户端的连接。

解决办法

设置一个服务器端应用,该应用利用 java.net.ServerSocket 来监听指定端口上的请求。下面的 Java 类代表了一个将被部署到服务器上的类,它监听端口 1234 上的传入请求。当收到请求时,传入的消息被打印到命令行,响应被发送回客户端。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {

public static void main(String a[]) {
        final int httpd = 1234;
        ServerSocket ssock = null;
        try {
            ssock = new ServerSocket(httpd);
            System.out.println("have opened port 1234 locally");

            Socket sock = ssock.accept();
            System.out.println("client has made socket connection");

    communicateWithClient(sock);

System.out.println("closing socket");
} catch (Exception e) {
System.out.println(e);
} finally {
try{
ssock.close();
} catch (IOException ex) {
System.out.println(ex);
}
}
}
    public static void communicateWithClient(Socket socket) {
        BufferedReader in = null;
        PrintWriter out = null;

        try {
            in = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(
                      socket.getOutputStream(), true);

            String s = null;
            out.println("Server received communication!");
            while ((s = in.readLine()) != null) {
                System.out.println("received from client: " + s);
                out.flush();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
                out.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

该配方与配方 21-2 协同工作,由此该示例启动服务器,并且该程序的执行将简单地打印“已在本地打开端口 1234”,但是与配方 21-2 中构建的客户端一起执行该程序将导致 SocketServer 的以下输出:

have opened port 1234 locally
client has made socket connection
received from client: Here is a test.
closing socket
注意

要运行这两个配方,使它们相互配合,首先启动 SocketServer 程序,这样客户机就可以使用服务器程序中打开的端口创建一个套接字。SocketServer 启动后,启动 SocketClient 程序来查看两者的协同工作。

警告

这个 SocketServer 程序在您的机器上打开一个端口(1234)。请确保您的计算机上运行了防火墙设置;否则,您将向所有人开放 1234 端口。这可能会导致您的机器受到攻击。开放的端口为攻击者创造了入侵机器的漏洞,就像让你家的门开着一样。请注意,该方法中的示例具有最小的攻击配置文件,因为服务器仅运行一次,并且在会话关闭前仅打印来自客户端的一条消息。

它是如何工作的

服务器应用可用于通过来自一个或多个客户端应用的直接通信在服务器上执行工作。客户端应用通常与服务器应用通信,向服务器发送消息或数据进行处理,然后断开连接。服务器应用通常会侦听客户端应用,然后在连接被接收和接受后,针对客户端请求执行一些处理。为了让客户端应用连接到服务器应用,服务器应用必须侦听连接,然后以某种方式处理连接数据。您不能简单地针对任何给定的主机和端口号组合运行客户端,因为这样做可能会导致拒绝连接错误。服务器端应用必须做三件事:打开一个端口,接受并建立客户端连接,然后以某种方式与客户端连接进行通信。在这个菜谱的解决方案中,SocketServer 类完成了这三项工作。

从 main()方法开始,该类首先在端口 1234 上打开一个新的套接字。这是通过创建一个新的 ServerSocket 实例并向其传递一个端口号来实现的。端口号不得与服务器上当前使用的任何其他端口冲突。值得注意的是,低于 1024 的端口通常保留给操作系统使用,因此选择一个高于该范围的端口号。如果试图打开一个正在使用的端口,将无法成功创建 ServerSocket,程序将会失败。接下来,调用 ServerSocket 对象的 accept()方法,返回一个新的 Socket 对象。调用 accept()方法将不会做任何事情,直到客户端尝试在已经设置的端口上连接到服务器程序。accept()方法将一直等待,直到请求连接,然后它将返回绑定到在 ServerSocket 上设置的端口的新 Socket 对象。这个套接字还包含尝试连接的客户端的远程端口和主机名,因此它包含两个端点的信息,并唯一地标识传输控制协议(TCP)连接。

此时,服务器程序可以与客户端程序进行通信,它使用 PrintWriter 和 BufferedReader 对象进行通信。在这个方法的解决方案中,communicateWithClient()方法包含从客户端程序接受消息、将消息发送回客户端,然后将控制权返回给关闭服务器套接字的 main()方法所需的所有代码。通过使用套接字的输入流生成新的 InputStreamReader 实例,可以创建新的 BufferedReader 对象。类似地,可以使用套接字的输出流创建新的 PrintWriter 对象。请注意,这段代码必须包装在一个 try-catch 块中,以防这些对象没有成功创建。

in = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(     
socket.getOutputStream(), true); 

一旦成功创建了这些对象,服务器就可以与客户机通信了。它使用一个循环来完成这项工作,从 BufferedReader 对象(客户端输入流)中读取数据,并使用 PrintWriter 对象将消息发送回客户端。在这个方法的解决方案中,服务器通过发出 break 关闭连接,这导致循环结束。控制然后返回到 main()方法。

out.println("Server received communication!");
while ((s = in.readLine()) != null) {
    System.out.println("received from client: " + s);
    out.flush();
    break;
}

在真实的服务器程序中,服务器很可能会无休止地监听,而不使用中断来结束通信。为了处理多个并发的客户端,每个客户端连接将产生一个单独的线程来处理通信。服务器也会对客户端通信做一些有用的事情。在 HTML 服务器的情况下,它会向客户机发回一条 HTML 消息。在 SMTP 服务器上,客户端会向服务器发送一封电子邮件,然后服务器会处理并发送该邮件。套接字通信几乎用于任何 TCP 传输,客户端和服务器都创建新的套接字来执行成功的通信。

21-2.定义到服务器的网络连接

问题

您需要建立到远程服务器的连接。

解决办法

使用远程服务器的名称和端口号创建一个到远程服务器的套接字连接,服务器在该端口监听传入的客户端请求。下面的示例类创建一个到远程服务器的套接字连接。然后,代码向服务器发送文本消息并接收响应。在本例中,客户端尝试联系的服务器名为 server-name,端口号为 1234。

小费

要创建与客户机上运行的本地程序的连接,请将服务器名设置为 127.0.0.1。这是在这个食谱的源列表中完成的。通常像这样的本地连接仅用于测试目的。

public class SocketClient {

    public static Socket socket = null;
    public static PrintWriter out;
    public static BufferedReader in;

    public static void main(String[] args) {
        createConnection("127.0.0.1", 1234);
    }

    public static void createConnection(String host, int port) {

        try {
            //Create socket connection
            socket = new Socket(host, port);
            // Obtain a handle on the socket output
            out = new PrintWriter(socket.getOutputStream(),
                    true);
            // Obtain a handle on the socket input
            in = new BufferedReader(new InputStreamReader(
                    socket.getInputStream()));
            testConnection();
            System.out.println("Closing the connection...");
            out.flush();
            out.close();
            in.close();
            socket.close();
            System.exit(0);
            } catch (UnknownHostException e) {
            System.out.println(e);
            System.exit(1);
            } catch (IOException e) {
            System.out.println(e);
            System.exit(1);
        }
    }

    public static void testConnection() {
        String serverResponse = null;
        if (socket != null && in != null && out != null) {
            System.out.println("Successfully connected, now testing...");

            try {
                // Send data to server
                out.println("Here is a test.");
                // Receive data from server
                while((serverResponse = in.readLine()) != null)
                System.out.println(serverResponse);
                } catch (IOException e) {
                System.out.println(e);
                System.exit(1);
            }
        }
    }
}

如果您针对成功接受请求的服务器测试该客户机,您将看到以下结果:

Successfully connected, now testing...
注意

这个程序本身不会做任何事情。要创建一个服务器端的套接字应用来接受这个完整测试的连接,请参见配方 21-1。如果您尝试运行此类而不指定正在侦听所提供端口的服务器主机,将会收到此异常:java.net.ConnectException:连接被拒绝。

它是如何工作的

每个客户机/服务器连接都是通过套接字进行的,套接字是两个不同程序之间通信链路的端点。套接字有分配给它们的端口号,这些端口号充当 TCP/IP 层在尝试连接时使用的标识符。接受客户机请求的服务器程序通常在指定的端口号上侦听新的连接。当客户端想要向服务器发出请求时,它会利用服务器的主机名和服务器侦听的端口创建一个新的套接字,并尝试与该套接字建立连接。如果服务器接受套接字,则连接成功。

这个菜谱讨论的是套接字连接的客户端,所以我们现在不会详细讨论服务器端发生了什么。然而,关于连接的服务器端的更多信息包含在配方 21-1 中。这个配方的解决方案中的示例类代表了客户端程序如何尝试和建立与服务器端程序的连接。在这个配方中,名为 createConnection()的方法执行实际的连接。它接受将用于创建套接字的服务器主机名和端口号。在 createConnection()方法中,服务器主机名和端口号被传递给 Socket 类构造函数,从而创建一个新的 Socket 对象。接下来,使用 Socket 对象的输出流创建 PrintWriter 对象,使用 Socket 对象的输入流创建 BufferedReader 对象。

//Create socket connection
socket = new Socket(host, port);
// Obtain a handle on the socket output
out = new PrintWriter(socket.getOutputStream(),
                                  true);
// Obtain a handle on the socket input
in = new BufferedReader(new InputStreamReader(
                    socket.getInputStream()));

在创建套接字并获得套接字的输出流和输入流之后,客户端可以写入 PrintWriter,以便向服务器发送数据。类似地,为了从服务器接收响应,客户端从 BufferedReader 对象中读取。testConnection()方法用于使用新创建的套接字模拟客户端和服务器程序之间的对话。为此,需要检查 socket、in 和 out 变量,以确保它们不等于 null。如果它们不等于 null,则客户端尝试通过使用 out.println("这里是一个测试,"将消息发送到输出流来将消息发送到服务器。).然后创建一个循环,通过调用 in.readLine()方法监听来自服务器的响应,直到没有收到任何其他内容。然后它打印接收到的消息。

if (socket != null && in != null && out != null) {
    System.out.println("Successfully connected, now testing...");

    try {
        // Send data to server
        out.println("Here is a test.");
        // Receive data from server
        while((serverResponse = in.readLine()) != null)
            System.out.println(serverResponse);
    } catch (IOException e) {
        System.out.println(e);
        System.exit(1);
    }
}

java.net.Socket 类符合 java 编程语言的本质。它使开发人员能够针对独立于平台的 API 进行编码,以便与特定于不同平台的网络协议进行通信。它从开发人员那里抽象出每个平台的细节,并为实现客户机/服务器通信提供了一个简单而一致的实现。

21-3.为 InfiniBand 绕过 TCP 以获得性能提升

问题

您的应用部署在 Linux 或 Solaris 上,需要快速高效地移动数据,并且您需要消除任何可能减慢速度的瓶颈。

解决办法

使用 Sockets Direct Protocol (SDP)绕过 TCP,这可能是流程中的瓶颈。为此,请创建一个 SDP 配置文件,并设置系统属性来指定配置文件的位置。

注意

SDP 被添加到 Java SE 7 发行版中,仅用于部署在 Solaris 或 Linux 操作系统中的应用。开发 SDP 是为了支持 InfiniBand 结构上的流连接,这是 Solaris 和 Linux 都支持的。Java SE 7 版本支持 1.4.2 和 1.5 版本的 open fabrics Enterprise Distribution(OFED)。

此配置文件是一个可用于启用 SDP 的示例:

# Use SDP when binding to 192.0.2.1
bind 192.0.2.1 *

# Use SDP when connecting to all application services on 192.0.2.*
connect 192.0.2.0/24     1024-*

# Use SDP when connecting to the HTTP server or a database on myserver.org
connect myserver.org   8080
connect myserver.org   1521

以下摘录摘自终端。它是名为 SDPExample 的 Java 应用的执行,指定了 SDP 系统属性:

% java -Dcom.sun.sdp.conf=sdp.conf -Djava.net.preferIPv4Stack=true  SDPExample

它是如何工作的

有时,在执行网络通信时,应用必须尽可能快。通过 TCP 进行传输有时会降低性能,因此绕过 TCP 可能是有益的。自从 Java SE 7 发布以来,某些平台已经包含了对 SDP 的支持。SDP 支持 InfiniBand 结构上的流连接。Solaris 和 Linux 都支持 InfiniBand,因此 SDP 在这些平台上非常有用。

为了支持 SDP,您不需要对您的应用进行任何编程更改。使用 SDP 的唯一区别是,您必须创建一个 SDP 配置文件,并且在运行应用时,必须通过传递一个标志来告知 JVM 使用该协议。因为实现是透明的,所以可以为任何平台编写应用,并且那些支持 SDP 的应用可以只包含配置文件而绕过 TCP。

SDP 配置文件是一个文本文件,由绑定和连接规则组成。绑定规则指示当 TCP 套接字绑定到与给定规则匹配的地址和端口时,应该使用 SDP 协议传输。连接规则指示当未绑定的 TCP 套接字尝试连接到与给定规则匹配的地址和端口时,应该使用 SDP 协议传输。规则以指示规则类型的 bind 或 connect 关键字开头,后跟主机名或 IP 地址,以及一个端口号或端口号范围。根据在线文档,规则具有以下形式:

("bind"|"connect")1*LWSP-char(hostname|ipaddress)["/"prefix])1*LWSP-char("*"|port)É
["-"("*"|port)]

在这里显示的规则格式中,1LWSP 字符意味着任意数量的制表符或空格可以分隔标记。方括号内的内容表示可选文本,引号表示文字文本。在该配方的解决方案中,第一条规则表明 SDP 可用于本地地址 192.0.2.1 的 IP 地址上的任何端口(表示通配符)。分配给 InfiniBand 适配器的每个本地地址都应在配置文件中用绑定规则指定。配置文件中的第一个连接规则指定,只要连接到 IP 地址 192.0.2,就应该使用 SDP。*,使用 1024 或更大的端口。

connect 192.0.2.0/24     1024-*

这条规则使用了一些应该注意的特殊语法。具体来说,IP 地址的/24 后缀表示 32 位 IP 地址的前 24 位应该与指定的地址匹配。因为 IP 地址的每个部分都是 8 位,这意味着 192.0.2 应该完全匹配,最后一个字节可以是任何值。端口标识符中的破折号-*指定了 1024 或更大的范围,因为使用了通配符。配置文件中的第三和第四个连接规则指定 SDP 应该与主机名 myserver.org 和端口 8080 或 1521 一起使用。

接下来,为了启用 sdp,应该在启动应用时指定–DCOM . sun . SDP . conf 属性以及 SDP 配置文件的位置。另外,请注意,在解决方案中,属性-Djava.net.preferIPv4Stack 被设置为 true。这表示将使用 IPv4 地址格式。这是必要的,因为映射到 IPv6 的 IPv4 地址目前在 Solaris OS 或 Linux 下不可用。

尽管 SDP 只适用于 Solaris 或 Linux,但对于这些平台的用户来说,它是 JDK 的一个很好的补充。任何性能提升总是被视为一种额外的奖励,这种方法的解决方案当然属于这一类。

21-4.向一组接收者广播

问题

您希望将数据报广播到零个或多个由单个地址标识的主机。

解决办法

使用 DatagramChannel 类利用数据报多播。DatagramChannel 类使多个客户端能够连接到一个组并侦听从服务器广播的数据报。下面几组代码使用客户机/服务器方法演示了这种技术。这个类演示了一个多播客户端。

package org.java9recipes.chapter21.recipe21_4;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.StandardProtocolFamily;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.MembershipKey;

public class MulticastClient {

    public MulticastClient() {
    }

    public static void main(String[] args) {
        try {
            // Obtain Supported network Interface
            NetworkInterface networkInterface = null;
            java.util.Enumeration<NetworkInterface> enumNI = NetworkInterface.getNetworkInterfaces();
            java.util.Enumeration<InetAddress> enumIA;
            NetworkInterface ni;
            InetAddress ia;
            ILOOP:
            while (enumNI.hasMoreElements()) {
                ni = enumNI.nextElement();
                enumIA = ni.getInetAddresses();
                while (enumIA.hasMoreElements()) {
                    ia = enumIA.nextElement();
                    if (ni.isUp() && ni.supportsMulticast()
                            && !ni.isVirtual() && !ni.isLoopback()
                            && !ia.isSiteLocalAddress()) {
                        networkInterface = ni;
                        break ILOOP;
                    }
                }
            }

            // Address within range
            int port = 5239;
            InetAddress group = InetAddress.getByName("226.18.84.25");

            final DatagramChannel client = DatagramChannel.open(StandardProtocolFamily.INET);

            client.setOption(StandardSocketOptions.SO_REUSEADDR, true);
            client.bind(new InetSocketAddress(port));
            client.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);

            System.out.println("Joining group: " + group + " with network interface " + networkInterface);
            // Multicasting join
            MembershipKey key = client.join(group, networkInterface);
            client.open();

            // receive message as a client
            final ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
            buffer.clear();
            System.out.println("Waiting to receive message");
            // Configure client to be passive and non.blocking
            // client.configureBlocking(false);
            client.receive(buffer);
            System.out.println("Client Received Message:");
            buffer.flip();
            byte[] arr = new byte[buffer.remaining()];
            buffer.get(arr, 0, arr.length);

            System.out.println(new String(arr));
            System.out.println("Disconnecting...performing a single test pass only");
            client.disconnect();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

接下来,可以使用服务器类将数据报广播到多播客户端所连接的地址。下面的代码演示了一个多播服务器:

package org.java9recipes.chapter21.recipe21_4;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class MulticastServer extends Thread {

    protected ByteBuffer message = null;

    public MulticastServer() {
    }

    public static void main(String[] args) {

        MulticastServer server = new MulticastServer();
        server.start();

    }

    @Override
    public void run() {

        try {

            // send the response to the client at "address" and "port"
            InetAddress address = InetAddress.getByName("226.18.84.25");
            int port = 5239;

            DatagramChannel server = DatagramChannel.open().bind(null);
            System.out.println("Sending datagram packet to group " + address + " on port " + port);
            message = ByteBuffer.wrap("Hello to all listeners".getBytes());
            server.send(message, new InetSocketAddress(address, port));

            server.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务器可以向作为该组成员的每个客户端广播消息。应该首先启动客户端,然后启动服务器。一旦服务器启动,它将广播消息,客户端将接收到它。

它是如何工作的

多播是在单次传输中向一组听众广播消息的能力。多播的一个很好的类比是无线电。成千上万的人可以收听同一个广播事件并听到相同的信息。计算机在向听众发送信息时也可以做类似的事情。一组客户机可以调谐到相同的地址和端口号,以接收服务器向该地址和端口广播的消息。Java 语言通过数据报消息传递提供多播功能。数据报是独立的、无保障的消息,可以通过网络传递给客户端。(无保障意味着到达、到达时间和内容是不可预测的。)与通过 TCP 发送的消息不同,发送数据报是一个非阻塞事件,发送方不会收到收到消息的通知。数据报是使用用户数据报协议(UDP)而不是 TCP 发送的。只要消息的顺序、可靠性和数据完整性不是至关重要的,通过 UDP 发送多播消息的能力是 TCP 的一个优势。

Java 通过 MulticastChannel 接口促进了多播消息传递。实现 MulticastChannel 接口的类启用了多播,因此可以向组广播并接收组广播。DatagramChannel 就是这样一个类,它是面向数据报的套接字的可选通道。在这个配方的解决方案中,客户端和服务器程序都用于通过多播消息传递进行通信,并且 DatagramChannel 类用于通信的两端。如果 DatagramChannel 要用于接受多播消息,则必须以特定的方式进行配置。具体来说,需要在打开的 DatagramChannel 客户端上设置一些选项。我们将很快讨论这些选项。创建接收多播消息的客户端需要以下步骤。

  1. 打开 DatagramChannel。

  2. 设置多播所需的 DatagramChannel 选项。

  3. 将客户端加入多播组并返回 MembershipKey 对象。

  4. 打开客户端。

在该方案的解决方案中,客户端应用首先获取对将用于接收广播消息的网络接口的引用。多播需要设置网络接口。接下来,选择端口号以及多播 IP 地址。群组或注册的收听者将使用 IP 地址来收听广播。端口号不得使用,否则将引发异常。对于 IPv4 多播,IP 地址的范围必须从 224.0.0.0 到 239.255.255.255,包括 224.0.0 和 239.255.255。这个端口和 IP 地址与服务器用来广播消息的端口和 IP 地址相同。接下来,使用 StandardProtocolFamily.INET 打开一个新的 DatagramChannel。INET 或 StandardProtocolFamily。INET6,分别对应 IPv4 和 IPv6。DatagramChannel 上设置的第一个选项是 StandardSocketOptions。SO_REUSEADDR,并将其设置为 true。这表明多个客户端将能够“重用”该地址或同时使用它。这需要为多播的发生进行设置。然后,使用新的 InetSocketAddress 实例将客户端绑定到该端口。最后,标准的 SocketOptions。IP_MULTICAST_IF 选项设置为使用的网络接口。此选项代表由面向数据报的套接字发送的多播数据报的传出接口。

client.setOption(StandardSocketOptions.SO_REUSEADDR, true);
client.bind(new InetSocketAddress(port));
client.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface); 

一旦设置了这些选项并且端口已经绑定到 DatagramChannel,它就准备好加入侦听器组了。这可以通过调用 DatagramChanneljoin(inet address,NetworkInterface)方法,传递客户端将使用的组地址和网络接口来完成。因此,会产生一个 Java . nio . channels . membership key 对象,这是一个表示 ip 多播组成员资格的令牌。最后,调用 DatagramChannelopen()方法,打开通道监听广播。此时,客户端准备好接收多播消息,并等待接收消息。

MembershipKey key = client.join(group, networkInterface);
client.open();

客户端接下来的几行代码负责从服务器接收消息。为了接收广播的消息,创建一个 ByteBuffer,然后最终传递给 DatagramChannel 的 receive()方法。调用 receive()方法后,客户端将暂停,直到收到消息。您可以通过调用 datagram channel configure blocking(boolean)方法并传递一个 false 值来禁用此功能。接下来,通过使用 flip()方法将缓冲区索引重新定位在 0,然后将从索引 0 开始到最后一个索引的文本拉入 byte[]中,将 ByteBuffer 转换为字符串值并打印出来。最后,完成后一定要断开客户端。它包装了客户端代码部分。

// Configure client to be passive and non.blocking
// client.configureBlocking(false);
client.receive(buffer);
// client pauses until a message is received... in this case
System.out.println("Client Received Message:");
buffer.flip();
byte[] arr = new byte[buffer.remaining()];
buffer.get(arr, 0, arr.length);

System.out.println(new String(arr));
System.out.println("Disconnecting...performing a single test pass only");
client.disconnect();
注意

在这个配方的例子中,执行了一次传递,然后客户端被断开。对于扩展监听,您需要一个带有超时的循环,并为结束状态提供测试。

服务器代码相当简单。可以看到 MulticastServer 类扩展了 Thread。这意味着这个服务器应用可以在一个独立于应用中其他代码的线程中运行。如果有另一个类启动了 MulticastServer 类的 run()方法,它将在与启动它的类不同的线程中运行。run()方法必须存在于任何扩展 Thread 的类中。有关线程和并发的更多信息,请参考第十章。

大部分服务器代码驻留在 run()方法中。为了加入多播组,使用客户机注册时使用的同一 IP 地址创建一个新的 InetAddress 对象。服务器代码中也声明了相同的端口号,这两个对象稍后将在代码块中用于发送消息。一个新的 DatagramChannel 被打开并绑定到 null。null 值很重要,因为通过将 SocketAddress 设置为 null,套接字将被绑定到一个自动分配的地址。接下来,创建一个 ByteBuffer,其中包含一条将广播给任何侦听器的消息。然后使用 DatagramChannel 的 send(ByteBuffer,InetSocketAddress)方法发送消息。解决方案中的 send()方法接受作为 ByteBuffer 对象的消息,以及使用地址和端口创建的新 InetSocketAddress,该地址和端口在块的开头声明。告诉过你我们会回来的!

server.send(message, new InetSocketAddress(address, port));

此时,客户端将收到服务器发送的消息。至于这个配方的解决方案中展示的客户端,它将会断开连接。通常,在真实的场景中,不同的类最有可能启动服务器,并且它的 run()方法将包含一个循环,该循环将继续执行,直到所有消息都已广播完毕或者循环被告知停止。在用户启动关机之前,客户端可能不会断开连接。

注意

如果您的笔记本电脑或服务器使用不同于标准 IPv4 的网络协议,则结果可能会有所不同。请确保在将您的代码发送到生产环境之前进行足够多的测试。

21-5.生成和读取 URL

问题

您希望在应用中以编程方式生成 URL。一旦创建了 URL,您就想从其中读取数据,以便在您的应用中使用。

解决办法

利用 java.net.URL 类来创建 URL。根据您尝试使用的地址,有几种不同的方法来生成 URL。这个解决方案演示了创建 URL 对象的一些选项,以及说明不同之处的注释。一旦创建了 URL 对象,就会将其中一个 URL 读入 BufferedReader 并打印到命令行。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;

public class GenerateAndReadUrl {

    public static void main(String[] args) {
        try {
            // Generate absolute URL
            URL url1 = new URL("http://www.java.net");
            System.out.println(url1.toString());
            // Generate URL for pages with a common base
            URL url2 = new URL(url1, "search/node/jdk8");

            // Generate URL from different pieces of data
            URL url3 = new URL("http", "java.net", "search/node/jdk8");

            readFromUrl(url1);

        } catch (MalformedURLException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Open URL stream as an input stream and print contents to command line.
     *
     * @param url
     */
    public static void readFromUrl(URL url) {
        try {
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(
                    url.openStream()));

            String inputLine;

            while ((inputLine = in.readLine()) != null) {
                System.out.println(inputLine);
            }

            in.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

运行该程序将导致来自 URL 资源(标识为 url1)的 HTML 被打印到命令行。

它是如何工作的

由于 java.net.URL 类完成了所有繁重的工作,所以用 Java 代码创建 URL 相当简单。URL 是指向互联网上的资源的字符串。有时用 Java 代码创建 URL 很有用,这样您就可以从 URL 指向的 Internet 资源中读取内容,或者将内容推送到该资源中。在这个配方的解决方案中,创建了一些不同的 URL 对象,展示了可供使用的不同构造函数。

创建 URL 最简单的方法是将位于 Internet 上的资源的标准可读 URL 字符串传递给 java.net.URL 类,以创建 URL 的新实例。在该解决方案中,将一个绝对 URL 传递给构造函数来创建 url1 对象。

URL url1 = new URL("http://www.java.net");

创建 URL 的另一个有用的方法是向 URL 构造函数传递两个参数,并创建一个相对 URL。将相对 URL 建立在另一个 URL 的位置上是很有用的。例如,如果一个特定的站点有许多不同的页面,您可以创建一个 URL,指向相对于主站点 URL 的一个子页面。这个配方的解决方案中的 url2 对象就是这种情况。

URL url2 = new URL(url1, "search/node/jdk8");

如您所见,路径 search/node/jdk8 是相对于 url1 的。最终,url2 对象的人类可读格式被表示为www.java.net/search/node/jdk8。还有几个构造函数可以创建接受两个以上参数的 URL 对象。这些构造函数如下:

new URL (String protocol, String host, String port, String path);
new URL (String protocol, String host, String path);

在该解决方案中,演示了此处显示的两个构造函数中的第二个。资源的协议、主机名和路径被传递给构造函数来创建 url3 对象。这最后两个构造函数通常在动态生成 URL 时最有用。

21-6.解析 URL

问题

您希望以编程方式从 URL 收集信息,以便在应用中使用。

解决办法

使用内置的 URL 类方法解析 URL。在下面名为 Parse URL 的示例类中,创建了一个 URL 对象,然后使用内置的 Url 类方法对其进行解析,以收集有关该 Url 的信息。从 URL 中检索到信息后,它被打印到命令行,然后用于创建另一个 URL。

import java.net.MalformedURLException;
import java.net.URL;

public static void main(String[] args) {
URL url1 = null;
URL url2 = null;
try {
            // Generate absolute URL
            url1 = new URL("http://www.apress.com/catalogsearch/result/?q=juneau");

            String host = url1.getHost();
            String path = url1.getPath();
            String query = url1.getQuery();
            String protocol = url1.getProtocol();
            String authority = url1.getAuthority();
            String ref = url1.getRef();

            System.out.println("The URL " + url1.toString() + " parses to the following:\n");
            System.out.println("Host: " + host + "\n");
            System.out.println("Path: " + path + "\n");
            System.out.println("Query: " + query + "\n");
            System.out.println("Protocol: " + protocol + "\n");
            System.out.println("Authority: " + authority + "\n");
            System.out.println("Reference: " + ref + "\n");

            url2 = new URL(protocol + "://" + host + path + "?q=java");

        } catch (IOException ex) {
            ex.printStackTrace();

        }
    }

执行此代码时,将显示以下几行:

The URL http://www.apress.com/catalogsearch/result/?q=juneau parses to the following:

Host: www.apress.com

Path: /catalogsearch/result/

Query: q=juneau

Protocol: http

Authority: www.apress.com

Reference: null

它是如何工作的

当在应用中构造和使用 URL 时,提取与 URL 相关的信息有时是有益的。使用 URL 内置的类方法可以很容易地做到这一点,这些方法可以调用给定的 URL 并返回信息字符串。表 21-1 解释了 URL 类中用于获取信息的访问器方法。

表 21-1。用于查询 URL 的访问器方法
|

方法

|

返回的 URL 信息

|
| --- | --- |
| getAuthority() | 权威成分 |
| getFile() | 文件名组件 |
| getHost() | 主机名组件 |
| getPath() | 道路连通区 |
| getProtocol() | 协议标识符组件 |
| getRef() | 参考组件 |
| getQuery() | 查询组件 |

这些访问器方法中的每一个都返回一个字符串值,该值可用于提供信息或动态构造其他 URL,如示例中所做的那样。如果你看一下这个配方的结果,你可以看到通过表 21-1 中列出的访问器方法获得的关于 URL 的信息。大多数访问器都是不言自明的。然而,他们中的一些人可能需要进一步的解释。getFile()方法返回 URL 的文件名。文件名与连接 getPath()返回值和 getQuery()返回值的结果相同。getRef()方法可能不太简单。通过调用 getRef()方法返回的引用组件引用可能附加到 URL 末尾的“片段”。例如,一个片段使用井号(#)来表示,后跟一个字符串,该字符串通常对应于特定网页上的一个子部分。给定如下 URL,将使用 getRef()方法返回 recipe21_6。

www.java9recipes.org/chapters/chapter21#recipe21_6

虽然并不总是需要,但解析 URL 以获取信息的能力有时会非常有用。因为 Java 语言在 java.net.URL 类中内置了助手方法,所以收集有关 URL 的信息很容易。

21-7.发出 HTTP 请求和使用 HTTP 响应

问题

您希望从应用内部发起一个 HTTP 请求,并相应地处理响应。

解决办法

利用 HTTP/2 客户端 API,以同步或异步方式发出请求。在下面的示例代码中,向 Apress 网站发出了一个请求。该示例演示了一个同步请求,因此代码将阻塞,直到收到响应。

public static void synchronousRequest() {
    try {
        HttpResponse resp = HttpRequest.create(
                new URI("http://www.apress.com/us/")).GET().response();
        int statusCode = resp.statusCode();
        String body = resp.body(HttpResponse.asString());
        System.out.println("Status Code: " + statusCode);
        // Do something with body text
    } catch (URISyntaxException | IOException | InterruptedException ex) {
        Logger.getLogger(HttpClient.class.getName()).log(Level.SEVERE, null, ex);
    }
}

运行此示例的输出应该如下所示,除非站点关闭或存在网络通信问题:

Status Code: 200

要执行异步请求,只需调用 responseAsync()方法,而不是 response()。这样做将返回 CompleteableFuture,在此基础上您可以检查状态以确定响应是否已返回。

public static void asynchronousRequest() {
    try {
        CompletableFuture<HttpResponse> cf = HttpRequest.create(
                new URI("http://www.apress.com/us/")).GET().responseAsync();
        System.out.println("Request made...");

        System.out.println("Check if done...");
        while (!cf.isDone()) {
            System.out.println("Perform some other tasks while waiting...");
            // Periodically check CompletableFuture.isDone()
        }
        System.out.println("Response Received:");
        HttpResponse response = cf.get();
        int statusCode = response.statusCode();
        System.out.println("Status Code: " + statusCode);
        String body = response.body(HttpResponse.asString());
        // Do something with body text

    } catch (URISyntaxException | InterruptedException | ExecutionException ex) {
        Logger.getLogger(HttpClient.class.getName()).log(Level.SEVERE, null, ex);
    }
}

异步示例的输出如下所示:

Request made...
Check if done...
Perform some other tasks while waiting...
Perform some other tasks while waiting...
Perform some other tasks while waiting...
...
Response Received:
Status Code: 200

它是如何工作的

多年来,HTTP/1.1 客户端一直是 JDK 的一部分。事实上,自从它在 JDK 1.1 中出现以来,基本上没有什么变化。HTTP/1.1 已经过时,不再是通过 HTTP 进行通信的首选方法。作为 HTTP/1.1 的一部分,较新的标准 HTTP/2 解决了许多已经存在多年的问题。在 Java 9 中,添加了一个新的 HTTP/2 客户端 API,允许开发人员轻松地利用更新的方法,同时仍然保持向后兼容。

由于诸如行首阻塞和大量的请求/响应周期等问题,HTTP/1.1 的性能经常是一个问题。HTTP/2 协议是在 2015 年推出的,它解决了许多老问题。例如,现在发送消息时使用二进制帧,降低了解析消息的复杂性。现在一切都可以通过一个 TCP 连接发送,而不是创建多个 TCP 连接来发送大量消息。这仅仅触及了表面,HTTP/2 中已经有了更多的改进……但是这提供了一个对为什么需要这些改变的合理理解。

如前所述,Java 9 中添加了一个新的 HTTP/2 客户端 API,使得同步或异步执行 HTTP 请求和接收 HTTP 响应变得容易。在第一个示例中,演示了同步 API,调用 HttpRequest.create()方法,传递一个 URI,然后以构建器样式的模式调用 GET()和 response()方法。这会返回一个 HttpResponse 对象。

 HttpResponse resp = HttpRequest.create(
                new URI("http://www.apress.com/us/")).GET().response();

当然,这是一个阻塞调用,因为需要等到收到响应后才能完成进一步的处理。一旦接收到,HttpResponse 对象可用于返回正文、HTTP 状态代码和许多其他项目。在这个例子中,HTTP 状态代码仅仅是打印出来的,在很多情况下,状态代码和一个条件一起使用来决定如何执行处理。

看看第二个异步示例,当调用 HttpRequest.create()方法时,很容易注意到代码中的差异。将 URI 传递给 create()方法后,再次调用 GET()方法,然后调用 responseAsync()方法。对 responseAsync()的调用返回 CompletableFuture,在这种情况下,泛型用于强制返回 HttpResponse。然后可以检查 CompletableFuture,以确定是否使用 isDone()方法返回了响应。如果响应还没有被返回,则可以采取适当的动作来在稍后的时间再次保持检查,或者相应地处理接收到的响应。在本例中,while 循环用于继续循环,直到最终返回响应。为了使这段代码更适合生产,可以在完成一定次数的迭代后使用条件来暂停循环。

更新的 HTTP/2 客户端带来了一个更现代的 API 来处理 HTTP 到 Java 的转换。更新后的 API 确保用户可以执行同步或异步的请求/响应生命周期。

摘要

本章讲述了 Java 语言的一些基本网络特性。在最近的版本中,增加了一些不错的新特性,比如 SDP。然而,java.net 软件包的大部分内容多年来都没有改变,它非常健壮且易于使用。本章深入研究了使用套接字连接和 URL 以及通过 DatagramChannel 广播消息。最后,介绍了更新的 HTTP/2 客户机。

二十二、Java 模块化

Java 9 最重要的新特性之一是模块化系统,它是通过 Jigsaw 项目实现的。竖锯项目也可以被称为 JSR 376:Java 平台模块系统。该项目的目的是构建一个提供可靠配置的系统,以取代类路径系统。它还关注于在不同模块之间提供强大的封装。模块系统由构成 Java 平台的所有模块组成,因为该平台是作为该项目的一部分从头开始重新构建和模块化的。应用开发人员和库创建者也可以创建模块…无论是执行特定任务的单个模块,还是共同创建应用的多个模块。

在这一章中,将涉及模块开发和管理的基本原理。尽管 Java 模块化是一个非常大的主题,但这一章很简洁,提供了足够的信息来快速开始模块开发。对于那些有兴趣了解更多关于 Java 模块化细节的人,我推荐他们阅读更深入的书籍和文档。

22-1.构建模块

问题

您希望创建一个简单的模块,将消息打印到命令行或通过记录器。

解决办法

开发一个模块,以便它可以通过 java 可执行文件来执行。首先在文件系统的某个地方创建一个新目录…在这种情况下,将其命名为“recipe22-1”创建一个名为 module-info.java 的新文件,它是模块描述符。在该文件中,列出模块名称,如下所示:

module org.firstModule {}

接下来,在之前创建的 recipe22-1 目录中创建一个名为 org 的文件夹。接下来,在 org 文件夹中创建一个名为 firstModule 的文件夹。现在,通过在 org.firstModule 文件夹中添加一个名为 Main.java 的新文件来创建模块的主体。将以下代码放在 Main.java 文件中:

package org.firstModule;
public class Main {
    public static void main(String[] args) {
        System.out.println("This is my first module");
    }
}

它是如何工作的

最简单的模块可以用两个文件构建,一个是模块描述符,另一个是包含业务逻辑的 Java 类文件。本例的解决方案遵循这种模式来创建一个非常基本的模块,该模块执行将短语打印到命令行的单一任务。该模块打包在一个与模块名同名的目录中。在本例中,这个目录被命名为 org.firstModule,因为它遵循标准的模块命名约定。实际上,一个模块可以被命名为任何名称,只要它不与其他模块名称冲突。但是,建议使用包的反向域名模式。这导致模块名以其包含的包名为前缀。

在这个解决方案中,模块描述符包含模块名,后跟左大括号和右大括号。在更复杂的模块中,其他模块依赖项的名称可以放在大括号中,以及该模块导出供其他人使用的包的名称。模块描述符应该位于模块目录的根目录下。包含这个文件向 JVM 表明这是一个模块。这个目录可以做成一个 JAR 文件,我将在本章后面讨论,这就创建了一个模块化的 JAR。

开发简单模块必须创建的另一个文件是包含业务逻辑的 Java 类文件。这个文件应该放在 org/firstModule 目录中,包应该指示 org.firstModule。请注意,模块需要的任何依赖关系都必须在模块描述符中列出。在这个简单的模块中,没有依赖关系。在设置了这个目录结构并将这两个文件放入各自的位置后,模块开发就完成了。

22-2.编译和执行模块

问题

你已经开发了一个基本模块。现在您想编译模块并执行它。

解决办法

利用 javac 实用程序来编译模块,指定 d 标志来列出编译后的代码将放入的文件夹。在 d 选项之后,必须列出每个要编译的源文件,包括 module-info.java 描述符。用空格分隔每个文件路径。以下命令编译配方 22-1 中开发的源代码,并将结果放入名为 mods/org.firstModule 的目录中。

javac d src/mods/org.firstModule src/org.firstModule/module-info.java src/org.firstModule/org/firstModule/Main.java

现在代码已经编译好了,是时候执行模块了。这可以通过标准的 java 可执行文件来完成。但是,必须使用 Java 9 中新增的- module-path 选项来指示模块源的路径。-m 选项用于指定模块的主类。

java --module-path mods -m org.firstModule/org.firstModule.Main  

执行该模块的输出应该如下所示:

This is my first module

如果要编译的模块不止一个,那么可以使用与前面描述的技术类似的技术分别编译它们,也可以一次性编译它们。编译包含依赖项的两个模块的语法如下:

javac -d mods --module-source-path src $(find src -name "*.java")

它是如何工作的

如您所知,在 Java 应用可以被执行之前,它必须被编译。模块也是如此,它们在使用之前必须被编译。标准的 javac 实用程序得到了增强,只需列出 module-info.java 文件和每个后续文件的全限定路径,就可以适应模块的编译。模块中包含的 java 文件。d 选项用于指定编译源的目的地。在这个解决方案中,javac 实用程序被调用,目标被设置为位置 src/mods/org.firstModule。组成模块的 java 文件在后面列出,用空格隔开。如果一个特定的模块包含许多。java 源文件,然后只需在每个包后的路径中指定一个星号(*)通配符,而不是单独的文件名,就足以编译每个包了。包含在指定包中的 java 文件。

javac -d mods/src/org.firstModule src/org.firstModule/module-info.java src/org.firstModule/org/firstModule/*

用于执行大多数 java 应用的相同 Java 可执行文件可用于执行模块。在一些新选项的帮助下,java 可执行文件能够执行具有所有必需依赖项的模块。- module-path 选项指定编译后的模块所在的路径。如果有许多模块组成一个应用,请指定包含应用入口点的模块的路径。-m 选项用于指定路径应用入口点类及其完全限定名。在这个解决方案中,主类位于一个名为 org.firstModule 的目录和一个名为 org.firstModule 的包中。

22-3.创建模块依赖关系

问题

您希望开发一个依赖并利用另一个模块的模块。

解决办法

开发至少两个模块,其中一个模块依赖于另一个模块。然后在模块描述符中指定依赖关系。在前面的菜谱中开发的模块也将用于本解决方案,但它将稍作修改,以利用另一个名为 org.secondModule 的模块。

首先,通过在 src 目录中创建新目录来创建模块 org.secondModule。接下来,创建一个名为 module-info.java 的. java 文件,并把它放在这个位置。模块描述符的内容应该如下所示:

module org.secondModule {
    exports org.secondModule;
}

该模块将使 org.secondModule 包中包含的源代码对其他需要它的模块可用。模块的源代码应该放在一个名为 Calculator.java 的类中,这个文件应该放在 src/org . second module/org/second module 目录中。将以下代码复制到 Calculator.java 中:

package org.secondModule;
import java.math.BigDecimal;
public class Calculator {
    public static BigDecimal calculateRate(BigDecimal days, BigDecimal rate) {
        return days.multiply(rate);
    }
}

最初用于 org.firstModule 的代码(配方 22-1 和 22.2)应修改为使用 org.secondModule,如下所示:

package org.firstModule;
import org.secondModule.Calculator;
import java.math.BigDecimal;
public class Main {
    public static void main(String[] args) {
        System.out.println("This is my first module.");
        System.out.println("The hotel stay will cost " + Calculator.calculateRate(
             BigDecimal.TEN, new BigDecimal(22.95)
        ));
    }
}

org.firstModule 的模块描述符必须修改为需要依赖关系:

module org.firstModule {
    requires org.secondModule;
}

要编译模块,请指定 javac 命令,使用通配符来编译 src 目录中的所有代码:

javac -d mods --module-source-path src $(find src -name "*.java")

最后,要执行 org.firstModule 及其依赖项,请使用之前用于执行该模块的语法。模块系统负责收集所需的依赖关系。

它是如何工作的

一个模块可以包含零个或多个依赖项。模块的可读性取决于该模块的模块描述符中导出的内容。同样,一个模块必须需要另一个模块才能读取它。模块系统实行强封装。模块本身总是可读的,但是其他模块只能使用从该模块导出的那些包。此外,只有公共方法等可供其他模块使用。

要使一个模块依赖于另一个模块,必须在模块描述符中放置一个必需的声明,指定它所依赖的模块的名称。在解决方案中,org.firstModule 依赖于 org.secondModule,因为模块描述符声明了它。这意味着 org.firstModule 能够利用 org.secondModule 模块的 org.secondModule 包中的任何公共特性。如果 org.secondModule 中包含更多的包,那么这些包对 org.firstModule 不可用,因为它们没有在 org.secondModule 的模块描述符中导出。

Java 9 模块的模块描述符的使用胜过类路径,因为它是声明依赖关系的更健壮的方法。然而,如果一个 Java 9 模块被打包成一个 JAR(见配方 22-4),那么通过将 JAR 放入类路径中,它就可以在旧版本的 Java 上使用,而模块描述符将被忽略。

模块可以使用 javac 命令单独编译,如配方 22-2 中所示,或者可以使用通配符符号编译,如配方 22-2 和本配方解决方案中所示。模块的执行是相同的,无论它依赖于零个还是多个其他模块。

22-4.封装模块

问题

您的模块已经开发完成,您希望对其进行打包以使其可移植。

解决办法

利用增强的 jar 实用程序来打包模块,并制作可执行模块。要打包配方 22-2 中开发的模块,导航至包含 mods 和 src 目录的目录。从该目录中,通过命令行执行以下命令:

mkdir lib
jar --create --file=lib/org.firstModule@1.0.jar --module-version=1.0 --main-class=org.firstModule.Main -C mods/org.firstModule .

该实用程序会将模块打包到 lib 目录下的 JAR 文件中。然后,可以使用 java 可执行文件执行 JAR 文件,如下所示:

java -p lib -m org.firstModule

它是如何工作的

Java 9 的 jar 实用程序得到了增强,增加了许多新选项,包括一些使模块打包更容易的选项。表 22-1 列出了 jar 实用程序的选项。

表 22-1。jar 实用程序选项
|

选项

|

描述

|
| --- | --- |
| -c,-创建 | 创建档案 |
| -I,- generate-index=FILE | 为指定的 jar 文件生成索引信息 |
| -t,-列表 | 列出归档的目录 |
| -u,-更新 | 更新现有的 jar 文件 |
| -x,-提取 | 从 jar 文件中提取一个或多个文件 |
| -C 目录 | 转到指定的目录并包含文件 |
| -f,- file=FILE | jar 文件的名称 |
| -v,-详细 | 生成详细输出 |
| -e,- main-class=NAME | 将被打包到 jar 中的模块的主类或入口点 |
| -m,-清单=文件 | 在 jar 中包含指定的清单文件信息 |
| -M-没有-清单 | 省略清单 |
| -模块-版本=版本 | 模块版本 |
| -哈希模块=模式 | 计算并记录与指定模式匹配的模块的哈希 |
| -P,-模块路径 | 用于生成哈希的模块依赖项的位置 |
| -0,-不压缩 | 规定不得使用 zip 压缩 |

查看该表,有几个选项对于使用模块很重要。具体来说,如示例所示,- module-version 选项允许指定版本。另一个特定于模块的选项是- module-path,它指定用于生成散列的模块依赖的位置。

除了新选项之外,使用模块创建 JAR 文件与标准的 JAR 文件生成没有太大的不同。也许最困难的部分是在启动命令时确保您在正确的目录中。正如在解决方案中看到的,只需使用- main-class 或-e 选项指定调用 JAR 时将执行的主类。之后,在模块根目录中执行-C 目录更改,然后用“.”结束命令以指示当前目录。

一旦创建了 JAR 文件,该模块将变得可移植,这意味着它可以在其他系统上使用。

22-5.列出依赖关系或确定 JDK 内部 API 的使用

问题

您想确定现有的应用是否依赖于 Java 9 中任何不可访问的内部 JDK API。

解决办法

使用 jdeps 工具从命令行列出模块依赖关系。要查看给定模块的依赖项列表,请按如下方式指定- list-deps 选项:

jdeps --list-deps <<your-jar.jar>>

调用该命令将启动输出,其中包括指定 JAR 文件所依赖的每个包。例如,从 GlassFish 应用服务器模块目录中选择一个随机的 JAR 文件将会产生类似如下的内容:

jdeps --list-deps acc-config.jar
   java.base
   java.xml.bind
   unnamed module: acc-config.jar

还有一些应用可能会使用 JDK 内部的 API,这些 API 对于从 Java 9 开始的标准应用来说是不可访问的。jdeps 工具可以列出这样的依赖关系,从而可以确定应用是否可以在 Java 9 上顺利运行。要利用此功能,请按如下方式指定-JDK internal 选项:

jdeps –jdkinternals <<your-jar.jar>>

调用 jdeps 实用程序来检查包含对 JDK 内部 API 的依赖的 JAR,将产生如下输出:

jdeps -jdkinternals security.jar
security.jar -> java.base
   com.sun.enterprise.common.iiop.security.GSSUPName  -> sun.security.util.ObjectIdentifier                    JDK internal API (java.base)
   com.sun.enterprise.common.iiop.security.GSSUtilsContract -> sun.security.util.ObjectIdentifier                    JDK internal API (java.base)
   com.sun.enterprise.security.auth.login.LoginContextDriver -> sun.security.x509.X500Name                            JDK internal API (java.base)
   com.sun.enterprise.security.auth.login.LoginContextDriver$4 -> sun.security.x509.X500Name                            JDK internal API (java.base)
   com.sun.enterprise.security.auth.realm.certificate.CertificateRealm -> sun.security.x509.X500Name                            JDK internal API (java.base)
   com.sun.enterprise.security.auth.realm.ldap.LDAPRealm -> sun.security.x509.X500Name                            JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.pkcs.ContentInfo                         JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.pkcs.PKCS7                               JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.pkcs.SignerInfo                          JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.x509.AlgorithmId                         JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.x509.X500Name                            JDK internal API (java.base)

Warning: JDK internal APIs are unsupported and private to JDK implementation that are
subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependence on any JDK internal APIs.
For the most recent update on JDK internal API replacements, please check:
https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool

JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.security.x509.X500Name               Use javax.security.auth.x500.X500Principal @since 1.4

它是如何工作的

jdeps (Java Dependency Analysis)工具是在 Java 8 中引入的,它是一个命令行工具,用于列出 JAR 文件的静态依赖关系。

Java 9 封装了许多内部 JDK API,使得标准应用无法访问它们。在 Java 9 之前,有些情况下需要应用使用这样的内部 API。这些应用将无法在 Java 9 上按预期运行,因此在尝试在 Java 9 上运行旧代码之前,必须找到并解决这种依赖性。jdeps 工具对于发现 JAR 是否依赖于这些内部 API 非常有用,如果它们存在的话,可以列出依赖关系。如果您希望在。点文件格式,请指定-dotoutput 选项和-JDK internal,如下所示:

jdeps -dotoutput /java_dev/security-dependencies.dot  -jdkinternals security.jar

一般来说,jdeps 工具对于确定 JAR 依赖关系也很有帮助。该工具包含一个- list-deps 选项来实现这一点。简单地说,- list-deps 选项列出了指定 JAR 所依赖的每个模块。

22-6.在模块之间提供松散耦合

问题

您希望在模块之间提供松散耦合,这样一个模块可以调用另一个模块作为服务。

解决办法

利用 Java 9 模块化系统中内置的服务架构。服务消费者可以通过在模块描述符中指定“uses”子句来指定松散耦合,以指示模块使用特定的服务。下面的例子可以用于一个模块,该模块可能具有提供 web 服务发现 API 的任务。在本例中,org . Java 9 recipes . service discovery 模块既需要模块,也导出模块。然后,它还指定使用 org . Java 9 recipes . SPI . service registry 服务。

module org.java9recipes.serviceDiscovery {
    requires public.java.logging;
    exports org.java9recipes.serviceDiscovery;
    uses org.java9recipes.spi.ServiceRegistry;
}

类似地,服务提供者必须指定它正在提供特定服务的实现。可以通过在模块描述符中包含“provide”子句来做到这一点。在本例中,下面的模块描述符表明服务提供者模块为 org . Java 9 recipes . SPI . service registry 提供了 org . data registry . database registry 的实现。

module org.dataregistry {
    requires org.java9recipes.serviceDiscovery;
    provides org.java9recipes.spi.ServiceRegistry
        with org.dataregistry.DatbaseRegistry;
}

现在可以编译和使用相应的模块了,它们将强制松耦合。

它是如何工作的

模块服务的概念允许两个或更多模块之间的松散耦合。利用所提供服务的模块被称为服务消费者,而提供服务的模块被称为服务提供者。服务消费者不使用服务提供者的任何实现类,而是使用接口。为了使松散耦合能够工作,模块系统必须能够容易地识别先前解析的模块的任何使用,相反,通过一组可观察的模块来搜索服务提供者。为了便于识别服务的使用,我们在模块描述符中指定了“uses”子句,以表明模块将使用所提供的服务。另一方面,当我们在服务提供者的模块描述符中指定“provides”子句时,模块系统可以很容易地找到服务提供者。

利用模块服务 API,编译器和运行时很容易看到哪些模块利用了服务,以及哪些模块提供了服务。这加强了更强的解耦,因为编译器和链接工具可以确保提供者被适当地编译并链接到这样的服务。

22-7.链接模块

问题

您希望链接一组模块来创建模块化运行时映像。

解决办法

利用 jlink 工具来链接所述模块集,以及它们的可传递依赖关系。在下面的摘录中,从配方 22-1 中创建的模块创建了一个运行时映像。

jlink --module-path $JAVA_HOME/jmods:mods --add-modules org.firstModule --output firstmoduleapp

它是如何工作的

有时,生成模块的运行时映像是很方便的,以便于移植。jlink 工具提供了这一功能。在该解决方案中,名为 firstmoduleapp 的运行时映像是从名为 org.firstModule 的模块创建的。- module-path 选项首先指示 JVM jmods 目录的路径,然后是包含要合并到运行时映像中的模块的任何目录。- add-modules 选项用于指定应该包含在映像中的每个模块的名称。

jlink 工具包含一组选项,如表 22-2 所示。

|

选项

|

描述

|
| --- | --- |
| -添加模块 | 要解析的命名模块。 |
| -c,- compress= <0|1|2> | 启用压缩或资源。 |
| -禁用插件 | 禁用命名插件。 |
| - endian | 指定生成图像的字节顺序。 |
| -忽略签名信息 | 当链接的图像将包含已签名的模块化 jar 时,抑制致命错误。签名的模块化 jar 的签名相关文件将不包括在内。 |
| -极限-模块 | 限制可观察模块的数量。 |
| -列表-插件 | 列出可用插件。 |
| -p,-模块路径 | 模块路径。 |
| -无头文件 | 从路径中排除头文件。 |
| -没有手册页 | 从路径中排除手册页。 |
| -输出 | 输出位置。 |
| -插件模块路径 | 自定义插件模块路径。 |
| -保存选项 | 将 jlink 选项保存在指定文件中。 |
| -G,- strip-debug | 剥离调试信息。 |
| -版本 | 版本信息。 |
| @ | 从指定文件中读取选项。 |

摘要

本章提供了 Java 9 模块系统的简要概述。在本章中,你学习了如何定义一个模块,编译和执行它。您还学习了如何打包模块以及如何创建模块依赖关系。您了解了一些有用的工具,这些工具用于列出依赖项、使用 JDK 内部 API 和链接模块。最后,本章演示了如何通过使用模块服务来创建松散耦合。

posted @ 2024-08-06 16:33  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报