Java11-秘籍(一)

173 阅读52分钟

Java11 秘籍(一)

原文:zh.annas-archive.org/md5/2bf50d1e2a61626a8f3de4e5aae60b76

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本食谱书提供了一系列软件开发示例,这些示例通过简单直接的代码进行了说明,提供了逐步资源和节省时间的方法,帮助您高效解决数据问题。从安装 Java 开始,每个食谱都解决了一个特定的问题,并附有解决方案的讨论,以及解释其工作原理的见解。我们涵盖了关于核心编程语言的主要概念,以及构建各种软件所涉及的常见任务。您将按照食谱了解最新 Java 11 版本的新功能,使您的应用程序模块化、安全和快速。

本书的受众包括初学者、有中级经验的程序员,甚至专家;所有人都能够访问这些食谱,这些食谱演示了 Java 11 发布的最新功能。

预期读者包括初学者、有中级经验的程序员,甚至专家;所有人都能够访问这些食谱,这些食谱演示了 Java 11 发布的最新功能。

为了充分利用本书

为了充分利用本书,需要一些 Java 知识和运行 Java 程序的能力。此外,最好安装和配置了您喜爱的编辑器或 IDE 以供在食谱中使用。因为本书本质上是一本食谱集,每个食谱都是基于具体示例的,如果读者不执行提供的示例,将会失去本书的好处。读者如果在他们的 IDE 中复制每个提供的示例,执行它,并将其结果与书中显示的结果进行比较,将会从本书中获得更多。

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用以下最新版本解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Java-11-Cookbook-Second-Edition。我们还有其他代码包,来自我们丰富的图书和视频目录,可在**github.com/PacktPublishing/**上获得。去看看吧!

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:"使用ProcessHandle接口上的allProcesses()方法获取当前活动进程的流"

代码块设置如下:

public class Thing {
  private int someInt;
  public Thing(int i) { this.someInt = i; }
  public int getSomeInt() { return someInt; }
  public String getSomeStr() { 
    return Integer.toString(someInt); }
} 

当我们希望引起您对代码块的特定部分的注意时,相关行或项目以粗体设置:

Object[] os = Stream.of(1,2,3).toArray();
 Arrays.stream(os).forEach(System.out::print);
 System.out.println();
 String[] sts = Stream.of(1,2,3)
                      .map(i -> i.toString())
                      .toArray(String[]::new);
 Arrays.stream(sts).forEach(System.out::print);

任何命令行输入或输出都写成如下形式:

jshell> ZoneId.getAvailableZoneIds().stream().count()
$16 ==> 599

粗体:表示一个新术语、一个重要词或屏幕上看到的词。例如,菜单或对话框中的单词会在文本中以这种方式出现。这是一个例子:"右键单击“我的电脑”,然后单击“属性”。您会看到

您的系统信息。"

警告或重要说明看起来像这样。

提示和技巧看起来像这样。

部分

本书中,您会发现一些经常出现的标题(准备工作如何做它是如何工作的还有更多另请参阅)。为了清晰地说明如何完成一个食谱,使用以下部分:

准备工作

本节告诉您在食谱中可以期待什么,并描述了如何设置食谱所需的任何软件或初步设置。

如何做...

本节包含了遵循食谱所需的步骤。

工作原理...

本节通常包括对前一节发生的事情的详细解释。

还有更多...

本节包括有关食谱的其他信息,以使您对食谱更加了解。

另请参阅

本节提供了有关食谱的其他有用信息的链接。

联系我们

我们始终欢迎读者的反馈。

一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并发送电子邮件至customercare@packtpub.com

勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误确实会发生。如果您在本书中发现错误,我们将不胜感激您向我们报告。请访问www.packt.com/submit-erra…,选择您的书,点击勘误提交表格链接,并输入详细信息。

盗版: 如果您在互联网上发现我们作品的任何形式的非法副本,我们将不胜感激您向我们提供位置地址或网站名称。请通过copyright@packtpub.com与我们联系,并提供材料链接。

如果您有兴趣成为作者: 如果您在某个专题上有专业知识,并且有兴趣撰写或为一本书做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了本书,为什么不在购买书籍的网站上留下评论呢?潜在读者可以看到并使用您的客观意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问packt.com

第一章:安装和预览 Java 11

在本章中,我们将介绍以下内容:

  • 在 Windows 上安装 JDK 18.9 并设置 PATH 变量

  • 在 Linux(Ubuntu,x64)上安装 JDK 18.9 并配置 PATH 变量

  • 编译和运行 Java 应用程序

  • JDK 18.9 的新功能

  • 使用应用程序类数据共享

介绍

学习编程语言的每一个探索都始于设置环境以进行学习实验。为了与这一理念保持一致,在本章中,我们将向您展示如何设置开发环境,然后运行一个简单的模块化应用程序来测试我们的安装。之后,我们将向您介绍 JDK 18.9 中的新功能和工具。然后,我们将比较 JDK 9、18.3 和 18.9。我们将以 JDK 18.3 中引入的允许应用程序类数据共享的新功能结束本章。

在 Windows 上安装 JDK 18.9 并设置 PATH 变量

在本教程中,我们将介绍如何在 Windows 上安装 JDK 以及如何设置PATH变量,以便能够在命令行中的任何位置访问 Java 可执行文件(如javacjavajar)。

如何做...

  1. 访问jdk.java.net/11/并接受早期采用者许可协议,它看起来像这样:

  1. 接受许可协议后,您将获得一个基于操作系统和架构(32/64 位)的可用 JDK 捆绑包的网格。单击下载适用于您的 Windows 平台的相关 JDK 可执行文件(.exe)。

  2. 运行 JDK 可执行文件(.exe)并按照屏幕上的说明在系统上安装 JDK。

  3. 如果您在安装过程中选择了所有默认设置,您将在 64 位的C:/Program Files/Java和 32 位的C:/Program Files (x86)/Java找到安装的 JDK。

既然我们已经安装了 JDK,让我们看看如何设置PATH变量。

JDK 提供的工具,即javacjavajconsolejlink,都位于 JDK 安装的 bin 目录中。您可以通过两种方式从命令提示符中运行这些工具:

  1. 导航到安装工具的目录并运行它们,如下所示:
 cd "C:\Program Files\Java\jdk-11\bin"
      javac -version
  1. 导出路径到目录,以便在命令提示符中的任何目录中都可以使用工具。为了实现这一点,我们必须将 JDK 工具的路径添加到PATH环境变量中。命令提示符将在PATH环境变量中声明的所有位置中搜索相关工具。

让我们看看如何将 JDK 的 bin 目录添加到PATH变量中:

  1. 右键单击“我的电脑”,然后单击“属性”。您将看到系统信息。搜索“高级系统设置”,单击它以获得一个窗口,如下面的屏幕截图所示:

  1. 单击“环境变量”以查看系统中定义的变量。您会看到已经定义了相当多的环境变量,如下面的屏幕截图所示(变量将在不同系统之间有所不同;在下面的屏幕截图中,有一些预定义的变量和一些我添加的变量):

在“系统变量”下定义的变量可供系统的所有用户使用,而在“用户变量”下定义的变量仅供特定用户使用。

  1. 一个新变量,名为JAVA_HOME,其值为 JDK 9 安装的位置。例如,它将是C:\Program Files\Java\jdk-11(64 位)或C:\Program Files (x86)\Java\jdk-11(32 位):

  1. 使用 JDK 安装的 bin 目录的位置(在JAVA_HOME环境变量中定义)更新PATH环境变量。如果您已经在列表中看到了PATH变量定义,那么您需要选择该变量并点击编辑。如果没有看到PATH变量,则点击新建。

  2. 在上一步中的任何操作都会弹出一个窗口,如下面的屏幕截图所示(在 Windows 10 上):

下面的屏幕截图显示了其他 Windows 版本:

  1. 您可以在第一个屏幕截图中单击新建并插入%JAVA_HOME%\bin值,或者通过在变量值字段中添加; %JAVA_HOME%\bin来追加值。在 Windows 中,分号(;)用于分隔给定变量名的多个值。

  2. 设置完值后,打开命令提示符并运行javac -version。您应该能够看到javac 11-ea作为输出。如果您没有看到它,这意味着您的 JDK 安装的 bin 目录没有正确添加到PATH变量中。

在 Linux(Ubuntu,x64)上安装 JDK 18.9 并配置 PATH 变量

在这个示例中,我们将看看如何在 Linux(Ubuntu,x64)上安装 JDK,并如何配置PATH变量以使终端中的 JDK 工具(如javacjavajar)可以从任何位置使用。

如何做...

  1. 按照在 Windows 上安装 JDK 18.9 并设置 PATH 变量的步骤 1 和 2 来到达下载页面。

  2. 从下载页面上复制 Linux x64 平台的 JDK 的下载链接(tar.gz)。

  3. 使用$> wget <copied link>下载 JDK,例如,$> wget https://download.java.net/java/early_access/jdk11/26/BCL/jdk-11-ea+26_linux-x64_bin.tar.gz

  4. 下载完成后,您应该有相关的 JDK 可用,例如,jdk-11-ea+26_linux-x64_bin.tar.gz。您可以使用$> tar -tf jdk-11-ea+26_linux-x64_bin.tar.gz列出内容。您甚至可以使用$> tar -tf jdk-11-ea+26_linux-x64_bin.tar.gz | more将其传输到more来分页输出。

  5. 使用$> tar -xvzf jdk-11-ea+26_linux-x64_bin.tar.gz -C /usr/lib/usr/lib下提取tar.gz文件的内容。这将把内容提取到一个目录/usr/lib/jdk-11中。然后,您可以使用$> ls /usr/lib/jdk-11列出 JDK 11 的内容。

  6. 通过编辑 Linux 主目录中的.bash_aliases文件来更新JAVA_HOMEPATH变量:

 $> vim ~/.bash_aliases
      export JAVA_HOME=/usr/lib/jdk-11
      export PATH=$PATH:$JAVA_HOME/bin

.bashrc文件以应用新的别名:

 $> source ~/.bashrc
      $> echo $JAVA_HOME
      /usr/lib/jdk-11
      $>javac -version
      javac 11-ea
      $> java -version
      java version "11-ea" 2018-09-25
 Java(TM) SE Runtime Environment 18.9 (build 11-ea+22)
 Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11-ea+22, mixed 
      mode)

本书中的所有示例都是在 Linux(Ubuntu,x64)上安装的 JDK 上运行的,除非我们特别提到这些是在 Windows 上运行的地方。我们已经尝试为两个平台提供运行脚本。

编译和运行 Java 应用程序

在这个示例中,我们将编写一个非常简单的模块化Hello world程序来测试我们的 JDK 安装。这个简单的示例在 XML 中打印Hello world;毕竟,这是 Web 服务的世界。

准备工作

您应该已经安装了 JDK 并更新了PATH变量以指向 JDK 安装位置。

如何做...

  1. 让我们使用相关属性和注释定义模型对象,这些属性和注释将被序列化为 XML:
        @XmlRootElement
        @XmlAccessorType(XmlAccessType.FIELD) 
        class Messages{     
          @XmlElement 
          public final String message = "Hello World in XML"; 
        }

在上面的代码中,@XmlRootElement用于定义根标签,@XmlAccessorType用于定义标签名称和标签值的来源类型,@XmlElement用于标识成为 XML 中标签名称和标签值的来源。

  1. 让我们使用 JAXB 将Message类的一个实例序列化为 XML:
public class HelloWorldXml{
  public static void main(String[] args) throws JAXBException{
    JAXBContext jaxb = JAXBContext.newInstance(Messages.class);
    Marshaller marshaller = jaxb.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FRAGMENT,Boolean.TRUE);
    StringWriter writer = new StringWriter();
    marshaller.marshal(new Messages(), writer);
    System.out.println(writer.toString());
  } 
}
  1. 我们现在将创建一个名为com.packt的模块。要创建一个模块,我们需要创建一个名为module-info.java的文件,其中包含模块定义。模块定义包含模块的依赖关系和模块向其他模块导出的包:
    module com.packt{
      //depends on the java.xml.bind module
      requires java.xml.bind;
      //need this for Messages class to be available to java.xml.bind
      exports  com.packt to java.xml.bind;
    }

我们将在第三章中详细解释模块化。但这个例子只是为了让您体验模块化编程并测试您的 JDK 安装。

具有上述文件的目录结构如下:

  1. 让我们编译并运行代码。从hellowordxml目录中,创建一个新目录,用于放置编译后的类文件:
      mkdir -p mods/com.packt

将源代码HelloWorldXml.javamodule-info.java编译成mods/com.packt目录:

 javac -d mods/com.packt/ src/com.packt/module-info.java
      src/com.packt/com/packt/HelloWorldXml.java
  1. 使用java --module-path mods -m com.packt/com.packt.HelloWorldXml运行编译后的代码。您将看到以下输出:
<messages><message>Hello World in XML</message></messages>

如果您无法理解javajavac命令中传递的选项,请不要担心。您将在第三章中了解它们,模块化编程

Java 11 中有什么新功能?

Java 9 的发布是 Java 生态系统的一个里程碑。在项目 Jigsaw 下开发的模块化框架成为了 Java SE 发布的一部分。另一个主要功能是 JShell 工具,这是一个用于 Java 的 REPL 工具。Java 9 引入的许多其他新功能在发布说明中列出:www.oracle.com/technetwork/java/javase/9all-relnotes-3704433.html

在本教程中,我们将列举并讨论 JDK 18.3 和 18.9(Java 10 和 11)引入的一些新功能。

准备就绪

Java 10 发布(JDK 18.3)开始了一个为期六个月的发布周期——每年三月和九月——以及一个新的发布编号系统。它还引入了许多新功能,其中最重要的(对于应用程序开发人员)是以下内容:

  • 允许使用保留的var类型声明变量的本地变量类型推断(见第十五章,使用 Java 10 和 Java 11 进行编码的新方法

  • G1 垃圾收集器的并行完整垃圾收集,改善了最坏情况下的延迟

  • 一个新的方法Optional.orElseThrow(),现在是现有get()方法的首选替代方法

  • 用于创建不可修改集合的新 API:java.util包的List.copyOf()Set.copyOf()Map.copyOf()方法,以及java.util.stream.Collectors类的新方法:toUnmodifiableList()toUnmodifiableSet()toUnmodifiableMap()(参见第五章,流和管道

  • 一组默认的根证书颁发机构,使 OpenJDK 构建更受开发人员欢迎

  • 新的 Javadoc 命令行选项--add-stylesheet支持在生成的文档中使用多个样式表

  • 扩展现有的类数据共享功能,允许将应用程序类放置在共享存档中,从而提高启动时间并减少占用空间(参见使用应用程序类数据共享教程)

  • 一种实验性的即时编译器 Graal,可以在 Linux/x64 平台上使用

  • 一个干净的垃圾收集器(GC)接口,使得可以更简单地向 HotSpot 添加新的 GC,而不会干扰当前的代码库,并且更容易地从 JDK 构建中排除 GC

  • 使 HotSpot 能够在用户指定的替代内存设备上分配对象堆,例如 NVDIMM 内存模块

  • 线程本地握手,用于在执行全局 VM 安全点的情况下在线程上执行回调

  • Docker 意识:JVM 将知道它是否在 Linux 系统上的 Docker 容器中运行,并且可以提取容器特定的配置信息,而不是查询操作系统

  • 三个新的 JVM 选项,为 Docker 容器用户提供对系统内存的更大控制

在发布说明中查看 Java 10 的新功能的完整列表:www.oracle.com/technetwork/java/javase/10-relnote-issues-4108729.html

我们将在下一节更详细地讨论 JDK 18.9 的新功能。

如何做到...

我们挑选了一些我们认为对应用程序开发人员最重要和有用的功能。

JEP 318 – Epsilon

Epsilon 是一种所谓的无操作垃圾收集器,基本上什么都不做。它的用例包括性能测试、内存压力和虚拟机接口。它还可以用于短暂的作业或不消耗太多内存且不需要垃圾收集的作业。

我们在第十一章的内存管理和调试中的食谱理解 Epsilon,一种低开销的垃圾收集器中更详细地讨论了此功能。

JEP 321 – HTTP 客户端(标准)

JDK 18.9 标准化了 JDK 9 中引入并在 JDK 10 中更新的孵化 HTTP API 客户端。基于CompleteableFuture,它支持非阻塞请求和响应。新的实现是异步的,并提供了更好的可追踪的数据流。

第十章的网络中,通过几个食谱更详细地解释了此功能。

JEP 323 – 用于 Lambda 参数的本地变量语法

lambda 参数的本地变量语法与 Java 11 中引入的保留var类型的本地变量声明具有相同的语法。有关更多详细信息,请参阅第十五章的使用 lambda 参数的本地变量语法食谱。

JEP 333 – ZGC

Z 垃圾收集器ZGC)是一种实验性的低延迟垃圾收集器。其暂停时间不应超过 10 毫秒,与使用 G1 收集器相比,应用吞吐量不应降低超过 15%。ZGC 还为未来的功能和优化奠定了基础。Linux/x64 将是第一个获得 ZGC 支持的平台。

新 API

标准 Java API 有几个新增内容:

  • Character.toString(int codePoint): 返回表示由提供的 Unicode 代码点指定的字符的String对象:
var s = Character.toString(50);
System.out.println(s);  //prints: 2

  • CharSequence.compare(CharSequence s1, CharSequence s2): 按字典顺序比较两个CharSequence实例。返回有序列表中第二个参数的位置与第一个参数位置的差异:
var i = CharSequence.compare("a", "b");
System.out.println(i);   //prints: -1

i = CharSequence.compare("b", "a");
System.out.println(i);   //prints: 1

i = CharSequence.compare("this", "that");
System.out.println(i);   //prints: 8

i = CharSequence.compare("that", "this");
System.out.println(i);   //prints: -8

  • String类的repeat(int count)方法:返回由count次重复组成的String值:
String s1 = "a";
String s2 = s1.repeat(3); //prints: aaa
System.out.println(s2);

String s3 = "bar".repeat(3);
System.out.println(s3); //prints: barbarbar

  • String类的isBlank()方法:如果String值为空或仅包含空格,则返回true,否则返回false。在我们的示例中,我们将其与isEmpty()方法进行了对比,后者仅在length()为零时返回true
String s1 = "a";
System.out.println(s1.isBlank());  //false
System.out.println(s1.isEmpty());  //false

String s2 = "";
System.out.println(s2.isBlank());  //true
System.out.println(s2.isEmpty());  //true

String s3 = "  ";
System.out.println(s3.isBlank());  //true
System.out.println(s3.isEmpty());  //false
  • String类的lines()方法:返回一个Stream对象,该对象从源String值中提取行,行之间由行终止符\n\r\r\n分隔:
String s = "l1 \nl2 \rl3 \r\nl4 ";
s.lines().forEach(System.out::print); //prints: l1 l2 l3 l4 

  • String类的三个方法,用于从源String值中移除前导空格、尾随空格或两者:
String s = " a b ";
System.out.println("'" + s.strip() + "'");        // 'a b'
System.out.println("'" + s.stripLeading() + "'"); // 'a b '
System.out.println("'" + s.stripTrailing() + "'");// ' a b'

  • 两个构造java.nio.file.Path对象的Path.of()方法:
Path filePath = Path.of("a", "b", "c.txt");
System.out.println(filePath);     //prints: a/b/c.txt

try {
    filePath = Path.of(new URI("file:/a/b/c.txt"));
    System.out.println(filePath);  //prints: /a/b/c.txt
} catch (URISyntaxException e) {
    e.printStackTrace();
}
  • java.util.regex.Pattern类的asMatchPredicate()方法,它创建了一个java.util.function.Predicate函数接口的对象,然后允许我们测试String值是否与编译后的模式匹配。在下面的示例中,我们测试String值是否以a字符开头并以b字符结尾:
Pattern pattern = Pattern.compile("^a.*z$");
Predicate<String> predicate = pattern.asMatchPredicate();
System.out.println(predicate.test("abbbbz")); // true
System.out.println(predicate.test("babbbz")); // false
System.out.println(predicate.test("abbbbx")); // false

还有更多...

JDK 18.9 中引入了相当多的其他更改:

  • 移除了 Java EE 和 CORBA 模块

  • JavaFX 已从 Java 标准库中分离并移除

  • util.jar中的 Pack200 和 Unpack200 工具以及 Pack200 API 已被弃用

  • Nashorn JavaScript 引擎以及 JJS 工具已被弃用,并打算在将来删除它们

  • Java 类文件格式被扩展以支持新的常量池形式CONSTANT_Dynamic

  • Aarch64 内在函数得到改进,为 Aarch64 处理器实现了java.lang.Math sin、cos 和 log 函数的新内在函数 JEP 309—动态类文件常量

  • Flight Recorder 为故障排除 Java 应用程序和 HotSpot JVM 提供了低开销的数据收集框架

  • Java 启动器现在可以运行作为 Java 源代码单个文件提供的程序,因此这些程序可以直接从源代码运行

  • 低开销的堆分析,提供了一种对 Java 堆分配进行采样的方式,可以通过 JVM 工具接口访问

  • 传输层安全性TLS)1.3 增加了安全性并提高了性能

  • java.lang.Characterjava.lang.Stringjava.awt.font.NumericShaperjava.text.Bidi,java.text.BreakIteratorjava.text.Normalizer类中支持 Unicode 版本 10.0

阅读 Java 11(JDK 18.9)发行说明以获取更多详细信息和其他更改。

使用应用程序类数据共享

这个功能自 Java 5 以来就存在。它在 Java 9 中作为商业功能得到了扩展,不仅允许引导类,还允许将应用程序类放置在 JVM 共享的存档中。在 Java 10 中,这个功能成为了 open JDK 的一部分。它减少了启动时间,并且当同一台机器上运行多个 JVM 并部署相同的应用程序时,减少了内存消耗。

做好准备

从共享存档加载类的优势有两个原因:

  • 存档中存储的类是经过预处理的,这意味着 JVM 内存映射也存储在存档中。这减少了 JVM 实例启动时类加载的开销。

  • 内存区域甚至可以在同一台计算机上运行的 JVM 实例之间共享,这通过消除每个实例中复制相同信息的需要来减少总体内存消耗。

新的 JVM 功能允许我们创建一个要共享的类列表,然后使用这个列表创建一个共享存档,并使用共享存档快速加载存档类到内存中。

如何做…

  1. 默认情况下,JVM 可以使用随 JDK 提供的类列表创建一个存档。例如,运行以下命令:
java -Xshare:dump

它将创建一个名为classes.jsa的共享存档文件。在 Linux 系统上,该文件放置在以下文件夹中:

/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home/lib/server

在 Windows 系统上,它放置在以下文件夹中:

C:\Program Files\Java\jdk-11\bin\server

如果这个文件夹只能被系统管理员访问,以管理员身份运行命令。

请注意,并非所有类都可以共享。例如,位于类路径上的目录中的.class文件和由自定义类加载器加载的类不能添加到共享存档中。

  1. 告诉 JVM 使用默认的共享存档,使用以下命令:
java -Xshare:on -jar app.jar

上述命令将存档的内容映射到固定地址。当所需的地址空间不可用时,这种内存映射操作有时会失败。如果在使用-Xshare:on选项时发生这种情况,JVM 将以错误退出。或者,可以使用-Xshare:auto选项,它只是禁用该功能,并且如果由于任何原因无法使用共享存档,则从类路径加载类。

  1. 创建加载的应用程序类列表的最简单方法是使用以下命令:
java -XX:+UseAppCDS -XX:DumpLoadedClassList=classes.txt -jar app.jar

上述命令记录了classes.txt文件中加载的所有类。如果要使应用程序加载更快,请在应用程序启动后立即停止 JVM。如果需要更快地加载某些类,但这些类不会在应用程序启动时自动加载,请确保执行需要这些类的用例。

  1. 或者,您可以手动编辑classes.txt文件,并添加/删除任何需要放入共享存档的类。首次自动创建此文件并查看格式。这是一个简单的文本文件,每行列出一个类。

  2. 创建列表后,使用以下命令生成共享存档:

java -XX:+UseAppCDS -Xshare:dump -XX:SharedClassListFile=classes.txt -XX:SharedArchiveFile=app-shared.jsa --class-path app.jar

请注意,共享存档文件的名称不是classes.jsa,因此不会覆盖默认共享存档。

  1. 通过执行以下命令使用创建的存档:
java -XX:+UseAppCDS -Xshare:on -XX:SharedArchiveFile=app-shared.jsa -jar app.jar

同样,您可以使用-Xshare:auto选项,以避免 JVM 意外退出。

共享存档的使用效果取决于其中的类数量和应用程序的其他细节。因此,我们建议您在承诺使用特定类列表之前进行实验和测试各种配置。

第二章:OOP 的快速通道-类和接口

在本章中,我们将涵盖以下示例:

  • 实现面向对象设计OOD

  • 使用内部类

  • 使用继承和聚合

  • 编码到一个接口

  • 创建具有默认和静态方法的接口

  • 创建具有私有方法的接口

  • 使用Optional更好地处理空值

  • 使用实用类Objects

本章的示例不需要对 OOD 有任何先前的了解。但是,在 Java 中编写代码的一些经验将是有益的。本章中的代码示例完全可用,并与 Java 11 兼容。为了更好地理解,我们建议您尝试运行所呈现的示例。

我们还鼓励您根据您团队的经验,将本章中的提示和建议调整到您的需求中。考虑与同事分享您新获得的知识,并讨论所描述的原则如何应用到您的领域和当前项目中。

介绍

本章为您快速介绍了面向对象编程OOP)的概念,并涵盖了自 Java 8 以来引入的一些增强功能。我们还将尝试在适用的地方涵盖一些良好的 OOD 实践,并使用具体的代码示例加以演示。

一个人可以花费很多时间阅读关于 OOD 的文章和实用建议,无论是在书籍上还是在互联网上。对一些人来说,这样做可能是有益的。但是根据我们的经验,掌握 OOD 的最快方法是在自己的代码中尝试其原则。这正是本章的目标——让您有机会看到和使用 OOD 原则,以便立即理解其正式定义。

良好编写代码的主要标准之一是意图的清晰。良好的动机和清晰的设计有助于实现这一点。代码由计算机运行,但由人类维护——阅读和修改。牢记这一点将确保您的代码的长期性,甚至可能会得到一些后来处理它的人的感激和赞赏。

在本章中,您将学习如何使用五个基本的 OOP 概念:

  • 对象/类:将数据和方法放在一起

  • 封装:隐藏数据和/或方法

  • 继承:扩展另一个类的数据和/或方法

  • 接口:隐藏实现和为一种类型编码

  • 多态:使用指向子类对象的基类类型引用

如果您在互联网上搜索,您可能会注意到许多其他概念和对它们的补充,以及所有 OOD 原则,都可以从前面列出的五个概念中推导出来。这意味着对它们的扎实理解是设计面向对象系统的先决条件。

实现面向对象设计(OOD)

在这个示例中,您将学习前两个 OOP 概念——对象/类和封装。这些概念是 OOD 的基础。

准备就绪

术语对象通常指的是将数据和可以应用于这些数据的过程耦合在一起的实体。数据和过程都不是必需的,但其中一个是——通常情况下,两者都是——总是存在的。数据称为对象字段(或属性),而过程称为方法。字段值描述了对象的状态。方法描述了对象的行为。每个对象都有一个类型,由其类——用于对象创建的模板——定义。对象也被称为类的实例。

是字段和方法的定义集合,这些字段和方法将存在于基于该类创建的每个实例中。

封装是隐藏那些不应该被其他对象访问的字段和方法。

封装是通过在字段和方法的声明中使用publicprotectedprivate Java 关键字,称为访问修饰符来实现的。当未指定访问修饰符时,还有一种默认级别的封装。

如何做...

  1. 创建一个带有horsePower字段的Engine类。添加setHorsePower(int horsePower)方法,用于设置该字段的值,以及getSpeedMph(double timeSec, int weightPounds)方法,用于根据车辆开始移动以来经过的时间、车辆重量和发动机功率计算车辆的速度:
public class Engine { 
  private int horsePower; 
  public void setHorsePower(int horsePower) { 
     this.horsePower = horsePower; 
  } 
  public double getSpeedMph(double timeSec, int weightPounds){ 
    double v = 2.0 * this.horsePower * 746 * timeSec * 
                                       32.17 / weightPounds; 
    return Math.round(Math.sqrt(v) * 0.68); 
 } 
}
  1. 创建Vehicle类:
      public class Vehicle { 
          private int weightPounds; 
          private Engine engine; 
          public Vehicle(int weightPounds, Engine engine) { 
            this.weightPounds = weightPounds; 
            this.engine = engine; 
          } 
          public double getSpeedMph(double timeSec){ 
            return this.engine.getSpeedMph(timeSec, weightPounds); 
         } 
     } 
  1. 创建将使用前述类的应用程序:
public static void main(String... arg) { 
   double timeSec = 10.0; 
   int horsePower = 246; 
   int vehicleWeight = 4000;  
   Engine engine = new Engine(); 
   engine.setHorsePower(horsePower); 
   Vehicle vehicle = new Vehicle(vehicleWeight, engine); 
   System.out.println("Vehicle speed (" + timeSec + " sec)=" 
                   + vehicle.getSpeedMph(timeSec) + " mph"); 
 } 

正如你所看到的,engine对象是通过调用Engine类的默认构造函数而创建的,该构造函数没有参数,并且使用new Java 关键字在堆上为新创建的对象分配内存。

第二个对象vehicle是使用Vehicle类的显式定义的带有两个参数的构造函数创建的。构造函数的第二个参数是engine对象,它携带了horsePower值,使用setHorsePower(int horsePower)方法设置为246

engine对象包含getSpeedMph(double timeSec, int weightPounds)方法,可以被任何对象调用(因为它是public),就像在Vehicle类的getSpeedMph(double timeSec)方法中所做的那样。

它是如何工作的...

前述应用程序产生以下输出:

值得注意的是,Vehicle类的getSpeedMph(double timeSec)方法依赖于为engine字段分配的值的存在。这样,Vehicle类的对象委托速度计算给Engine类的对象。如果后者未设置(例如在Vehicle()构造函数中传递了null),将在运行时抛出NullPointerException,如果应用程序未处理,将被 JVM 捕获并强制其退出。为了避免这种情况,我们可以在Vehicle()构造函数中放置一个检查,检查engine字段值的存在:

if(engine == null){ 
   throw new RuntimeException("Engine" + " is required parameter."); 
}   

或者,我们可以在Vehicle类的getSpeedMph(double timeSec)方法中放置一个检查:

if(getEngine() == null){ 
  throw new RuntimeException("Engine value is required."); 
} 

这样,我们避免了NullPointerException的歧义,并告诉用户问题的确切来源。

正如你可能已经注意到的,getSpeedMph(double timeSec, int weightPounds)方法可以从Engine类中移除,并且可以完全在Vehicle类中实现:

public double getSpeedMph(double timeSec){
  double v =  2.0 * this.engine.getHorsePower() * 746 * 
                                timeSec * 32.17 / this.weightPounds;
  return Math.round(Math.sqrt(v) * 0.68);
}

为此,我们需要在Engine类中添加getHorsePower()公共方法,以便使其可以被Vehicle类的getSpeedMph(double timeSec)方法使用。目前,我们将getSpeedMph(double timeSec, int weightPounds)方法留在Engine类中。

这是你需要做出的设计决策之一。如果你认为Engine类的对象将被传递并被不同类的对象使用(不仅仅是Vehicle),那么你需要在Engine类中保留getSpeedMph(double timeSec, int weightPounds)方法。否则,如果你认为只有Vehicle类将负责速度计算(这是有道理的,因为这是车辆的速度,而不是发动机的速度),你应该在Vehicle类中实现这个方法。

还有更多...

Java 提供了扩展类的能力,并允许子类访问基类的所有非私有字段和方法。例如,你可以决定每个可以被询问其速度的对象都属于从Vehicle类派生的子类。在这种情况下,Car类可能如下所示:

public class Car extends Vehicle {
  private int passengersCount;
  public Car(int passengersCount, int weightPounds, Engine engine){
    super(weightPounds, engine);
    this.passengersCount = passengersCount;
  }
  public int getPassengersCount() {
    return this.passengersCount;
  }
}

现在,我们可以通过用Car类的对象替换Vehicle类对象来更改我们的测试代码:

public static void main(String... arg) { 
  double timeSec = 10.0; 
  int horsePower = 246; 
  int vehicleWeight = 4000; 
  Engine engine = new Engine(); 
  engine.setHorsePower(horsePower); 
  Vehicle vehicle = new Car(4, vehicleWeight, engine); 
  System.out.println("Car speed (" + timeSec + " sec) = " + 
                             vehicle.getSpeedMph(timeSec) + " mph"); 
} 

当执行前面的代码时,它产生与Vehicle类对象相同的值:

由于多态性,对Car类对象的引用可以赋给其基类Vehicle的引用。Car类对象有两种类型——它自己的类型Car和基类Vehicle的类型。

在 Java 中,一个类也可以实现多个接口,这样类的对象也会有每个实现接口的类型。我们将在随后的配方中讨论这一点。

使用内部类

在这个配方中,您将了解三种内部类的类型:

  • 内部类:这是一个在另一个(封闭)类内部定义的类。它的可访问性由publicprotectedprivate访问修饰符调节。内部类可以访问封闭类的私有成员,封闭类也可以访问其内部类的私有成员,但是无法从封闭类外部访问私有内部类或非私有内部类的私有成员。

  • 方法局部内部类:这是一个在方法内部定义的类。它的可访问性受限于方法内部。

  • 匿名内部类:这是一个没有声明名称的类,在对象实例化时基于接口或扩展类定义。

准备就绪

当一个类只被一个其他类使用时,设计者可能会决定不需要将这样的类设为公共类。例如,假设Engine类只被Vehicle类使用。

如何做...

  1. Engine类创建为Vehicle类的内部类:
        public class Vehicle {
          private int weightPounds;
          private Engine engine;
          public Vehicle(int weightPounds, int horsePower) {
            this.weightPounds = weightPounds;
            this.engine = new Engine(horsePower);
          }
          public double getSpeedMph(double timeSec){
            return this.engine.getSpeedMph(timeSec);
          }
          private int getWeightPounds(){ return weightPounds; }
          private class Engine {
            private int horsePower;
            private Engine(int horsePower) {
              this.horsePower = horsePower;
            }
            private double getSpeedMph(double timeSec){
              double v = 2.0 * this.horsePower * 746 * 
                         timeSec * 32.17 / getWeightPounds();
              return Math.round(Math.sqrt(v) * 0.68);
            }
          }
        }
  1. 请注意,Vehicle类的getSpeedMph(double timeSec)方法可以访问Engine类,即使它被声明为private。它甚至可以访问Engine类的getSpeedMph(double timeSec)私有方法。内部类也可以访问封闭类的所有私有元素。这就是为什么Engine类的getSpeedMph(double timeSec)方法可以访问封闭Vehicle类的私有getWeightPounds()方法。

  2. 更仔细地看一下内部Engine类的用法。只使用了Engine类的getSpeedMph(double timeSec)方法。如果设计者认为将来也会是这种情况,他们可能会合理地决定将Engine类设为方法局部内部类,这是内部类的第二种类型:

        public class Vehicle {
          private int weightPounds;
          private int horsePower;
          public Vehicle(int weightPounds, int horsePower) {
            this.weightPounds = weightPounds;
            this.horsePower = horsePower;
          }
          private int getWeightPounds() { return weightPounds; }
          public double getSpeedMph(double timeSec){
            class Engine {
              private int horsePower;
              private Engine(int horsePower) {
                this.horsePower = horsePower;
              }
              private double getSpeedMph(double timeSec){
                double v = 2.0 * this.horsePower * 746 * 
                          timeSec * 32.17 / getWeightPounds();
                return Math.round(Math.sqrt(v) * 0.68);
              }
            }
            Engine engine = new Engine(this.horsePower);
            return engine.getSpeedMph(timeSec);
          }
        }

在前面的代码示例中,根本没有必要有一个Engine类。速度计算公式可以直接使用,而不需要Engine类的介入。但也有一些情况下可能不那么容易做到。例如,方法局部内部类可能需要扩展其他类以继承其功能,或者创建的Engine对象可能需要经过一些转换,因此需要创建。其他考虑可能需要方法局部内部类。

无论如何,将不需要从封闭类外部访问的所有功能设为不可访问是一个好的做法。封装——隐藏对象的状态和行为——有助于避免意外更改或覆盖对象行为导致的意外副作用。这使得结果更加可预测。这就是为什么一个好的设计只暴露必须从外部访问的功能。通常是封闭类的功能首先促使类的创建,而不是内部类或其他实现细节。

它是如何工作的...

无论Engine类是作为内部类还是方法局部内部类实现的,测试代码看起来都是一样的:

public static void main(String arg[]) {
  double timeSec = 10.0;
  int engineHorsePower = 246;
  int vehicleWeightPounds = 4000;
  Vehicle vehicle = 
          new Vehicle(vehicleWeightPounds, engineHorsePower);
  System.out.println("Vehicle speed (" + timeSec + " sec) = " 
                    + vehicle.getSpeedMph(timeSec) + " mph");
}

如果我们运行前面的程序,我们会得到相同的输出:

现在,假设我们需要测试getSpeedMph()方法的不同实现:

public double getSpeedMph(double timeSec){ return -1.0d; }

如果这个速度计算公式对你来说没有意义,那么你是正确的,它确实没有意义。我们这样做是为了使结果可预测,并且与先前实现的结果不同。

有许多方法可以引入这个新的实现。例如,我们可以改变Engine类中getSpeedMph(double timeSec)方法的代码。或者,我们可以改变Vehicle类中相同方法的实现。

在这个示例中,我们将使用第三种内部类,称为匿名内部类。当你想尽可能少地编写新代码,或者你想通过临时覆盖旧代码来快速测试新行为时,这种方法特别方便。匿名类的使用将如下所示:

public static void main(String... arg) {
  double timeSec = 10.0;
  int engineHorsePower = 246;
  int vehicleWeightPounds = 4000;
  Vehicle vehicle = 
    new Vehicle(vehicleWeightPounds, engineHorsePower) {
        public double getSpeedMph(double timeSec){ 
           return -1.0d;
        }
    };
  System.out.println("Vehicle speed (" + timeSec + " sec) = "
                    + vehicle.getSpeedMph(timeSec) + " mph");
}

如果我们运行这个程序,结果将是这样的:

正如你所看到的,匿名类实现已经覆盖了Vehicle类的实现。新的匿名类中只有一个方法——getSpeedMph()方法,它返回了硬编码的值。但我们也可以覆盖Vehicle类的其他方法或者添加新的方法。我们只是想为了演示目的保持示例简单。

根据定义,匿名内部类必须是语句的一部分,该语句以分号结束(与任何语句一样)。这样的表达式由以下部分组成:

  • new操作符

  • 实现的接口或扩展类的名称后跟括号(),表示默认构造函数或扩展类的构造函数(后者是我们的情况,扩展类是Vehicle

  • 类体与方法

像任何内部类一样,匿名内部类可以访问外部类的任何成员,但有一个注意事项——要被内部匿名类使用,外部类的字段必须要么声明为final,要么隐式地变为final,这意味着它们的值不能被改变。一个好的现代 IDE 会在你试图改变这样的值时警告你违反了这个约束。

使用这些特性,我们可以修改我们的示例代码,并为新实现的getSpeedMph(double timeSec)方法提供更多的输入数据,而无需将它们作为方法参数传递:

public static void main(String... arg) {
  double timeSec = 10.0;
  int engineHorsePower = 246;
  int vehicleWeightPounds = 4000;
  Vehicle vehicle = 
    new Vehicle(vehicleWeightPounds, engineHorsePower){
      public double getSpeedMph(double timeSec){
        double v = 2.0 * engineHorsePower * 746 * 
             timeSec * 32.17 / vehicleWeightPounds;
        return Math.round(Math.sqrt(v) * 0.68);
      }
    };
  System.out.println("Vehicle speed (" + timeSec + " sec) = " 
                    + vehicle.getSpeedMph(timeSec) + " mph");
}

请注意,timeSecengineHorsePowervehicleWeightPounds变量可以被内部类的getSpeedMph(double timeSec)方法访问,但不能被修改。如果我们运行上述代码,结果将与之前一样:

在只有一个抽象方法的接口(称为函数式接口)的情况下,可以使用另一种构造,称为lambda 表达式,而不是匿名内部类。它提供了更简洁的表示。我们将在第四章 进入函数式中讨论函数式接口和 lambda 表达式。

还有更多...

内部类是一个非静态嵌套类。Java 还允许我们创建一个静态嵌套类,当内部类不需要访问外部类的非静态字段和方法时可以使用。下面是一个示例(Engine类中添加了static关键字):

public class Vehicle {
  private Engine engine;
  public Vehicle(int weightPounds, int horsePower) {
    this.engine = new Engine(horsePower, weightPounds)
  }
  public double getSpeedMph(double timeSec){
    return this.engine.getSpeedMph(timeSec);
  }
  private static class Engine {
    private int horsePower;
    private int weightPounds;
    private Engine(int horsePower, int weightPounds) {
      this.horsePower = horsePower;
    }
    private double getSpeedMph(double timeSec){
      double v = 2.0 * this.horsePower * 746 * 
                       timeSec * 32.17 / this.weightPounds;
      return Math.round(Math.sqrt(v) * 0.68);
    }
  }
}

由于静态类无法访问非静态成员,我们被迫在构造Engine类时传递重量值,并且我们移除了getWeightPounds()方法,因为它不再需要了。

使用继承和聚合

在这个示例中,你将学习更多关于两个重要的面向对象编程概念,继承和多态,这些概念已经在前面的示例中提到并被使用。结合聚合,这些概念使设计更具可扩展性。

准备就绪

继承是一个类获取另一个类的非私有字段和方法的能力。

扩展的类称为基类、超类或父类。类的新扩展称为子类或子类。

多态性是使用基类类型引用其子类对象的能力。

为了演示继承和多态的威力,让我们创建代表汽车和卡车的类,每个类都有它可以达到的重量、发动机功率和速度(作为时间函数)的最大载荷。此外,这种情况下的汽车将以乘客数量为特征,而卡车的重要特征将是其有效载荷。

如何做...

  1. 看看Vehicle类:
        public class Vehicle {
          private int weightPounds, horsePower;
          public Vehicle(int weightPounds, int horsePower) {
            this.weightPounds = weightPounds;
            this.horsePower = horsePower;
          }
          public double getSpeedMph(double timeSec){
            double v = 2.0 * this.horsePower * 746 * 
                     timeSec * 32.17 / this.weightPounds;
            return Math.round(Math.sqrt(v) * 0.68);
          }
        }

Vehicle类中实现的功能不特定于汽车或卡车,因此将这个类用作CarTruck类的基类是有意义的,这样每个类都可以将这个功能作为自己的功能。

  1. 创建Car类:
        public class Car extends Vehicle {
          private int passengersCount;
          public Car(int passengersCount, int weightPounds, 
                                             int horsepower){
            super(weightPounds, horsePower);
            this.passengersCount = passengersCount;
          }
          public int getPassengersCount() { 
            return this.passengersCount; 
          }
        }
  1. 创建Truck类:
         public class Truck extends Vehicle {
           private int payload;
           public Truck(int payloadPounds, int weightPounds, 
                                              int horsePower){
             super(weightPounds, horsePower);
             this.payload = payloadPounds;
           }
           public int getPayload() { 
             return this.payload; 
           }
         }

由于Vehicle基类既没有隐式构造函数也没有没有参数的显式构造函数(因为我们选择只使用带参数的显式构造函数),所以我们必须在Vehicle类的每个子类的构造函数的第一行调用基类构造函数super()

它是如何工作的...

让我们编写一个测试程序:

public static void main(String... arg) {
  double timeSec = 10.0;
  int engineHorsePower = 246;
  int vehicleWeightPounds = 4000;
  Vehicle vehicle = new Car(4, vehicleWeightPounds, engineHorsePower);
  System.out.println("Passengers count=" + 
                                 ((Car)vehicle).getPassengersCount());
  System.out.println("Car speed (" + timeSec + " sec) = " + 
                               vehicle.getSpeedMph(timeSec) + " mph");
  vehicle = new Truck(3300, vehicleWeightPounds, engineHorsePower);
  System.out.println("Payload=" + 
                           ((Truck)vehicle).getPayload() + " pounds");
  System.out.println("Truck speed (" + timeSec + " sec) = " + 
                               vehicle.getSpeedMph(timeSec) + " mph");
}

注意,Vehicle类型的vehicle引用指向Car子类的对象,稍后指向Truck子类的对象。这是由多态性实现的,根据多态性,对象具有其继承线中每个类的类型,包括所有接口。

如果需要调用仅存在于子类中的方法,必须将这样的引用转换为子类类型,就像在前面的示例中所做的那样。

前面代码的结果如下:

我们不应该对看到相同的速度计算结果感到惊讶,因为相同的重量和发动机功率用于计算每个车辆的速度。但是,直观上,我们感觉到一个装载重的卡车不应该能够在相同的时间内达到与汽车相同的速度。为了验证这一点,我们需要在速度的计算中包括汽车的总重量(乘客和行李)和卡车的总重量(有效载荷)。一种方法是在每个子类中覆盖Vehicle基类的getSpeedMph(double timeSec)方法。

我们可以在Car类中添加getSpeedMph(double timeSec)方法,它将覆盖基类中具有相同签名的方法。这个方法将使用特定于汽车的重量计算:

public double getSpeedMph(double timeSec) {
  int weight = this.weightPounds + this.passengersCount * 250;
  double v = 2.0 * this.horsePower * 746 * timeSec * 32.17 / weight;
  return Math.round(Math.sqrt(v) * 0.68);
}

在前面的代码中,我们假设一个带行李的乘客平均重量为250磅。

类似地,我们可以在Truck类中添加getSpeedMph(double timeSec)方法:

public double getSpeedMph(double timeSec) {
  int weight = this.weightPounds + this.payload;
  double v = 2.0 * this.horsePower * 746 * timeSec * 32.17 / weight;
  return Math.round(Math.sqrt(v) * 0.68);
}

对这些修改的结果(如果我们运行相同的测试类)将如下:

结果证实了我们的直觉——装载完全的汽车或卡车的速度不会达到空车的速度。

子类中的新方法覆盖了Vehicle基类的getSpeedMph(double timeSec),尽管我们是通过基类引用来访问它的:

Vehicle vehicle =  new Car(4, vehicleWeightPounds, engineHorsePower);
System.out.println("Car speed (" + timeSec + " sec) = " + 
                              vehicle.getSpeedMph(timeSec) + " mph");

覆盖的方法是动态绑定的,这意味着方法调用的上下文是由实际对象的类型决定的。因此,在我们的示例中,引用vehicle指向Car子类的对象,vehicle.getSpeedMph(double timeSec)调用子类的方法,而不是基类的方法。

在这两个新方法中存在明显的代码冗余,我们可以通过在Vehicle基类中创建一个方法,然后在每个子类中使用它来重构。

protected double getSpeedMph(double timeSec, int weightPounds) {
  double v = 2.0 * this.horsePower * 746 * 
                              timeSec * 32.17 / weightPounds;
  return Math.round(Math.sqrt(v) * 0.68);
}

由于这个方法只被子类使用,它可以是protected,因此只能被子类访问。

现在,我们可以更改Car类中的getSpeedMph(double timeSec)方法,如下所示:

public double getSpeedMph(double timeSec) {
  int weightPounds = this.weightPounds + this.passengersCount * 250;
  return getSpeedMph(timeSec, weightPounds);
}

在上述代码中,调用getSpeedMph(timeSec, weightPounds)方法时不需要使用super关键字,因为这样的签名方法只存在于Vehicle基类中,对此没有任何歧义。

Truck类的getSpeedMph(double timeSec)方法中也可以进行类似的更改:

public double getSpeedMph(double timeSec) {
  int weightPounds = this.weightPounds + this.payload;
  return getSpeedMph(timeSec, weightPounds);
}

现在,我们需要通过添加转换来修改测试类,否则会出现运行时错误,因为Vehicle基类中不存在getSpeedMph(double timeSec)方法:

public static void main(String... arg) {
    double timeSec = 10.0;
    int engineHorsePower = 246;
    int vehicleWeightPounds = 4000;
    Vehicle vehicle = new Car(4, vehicleWeightPounds, 
    engineHorsePower);
    System.out.println("Passengers count=" + 
    ((Car)vehicle).getPassengersCount());
    System.out.println("Car speed (" + timeSec + " sec) = " +
                       ((Car)vehicle).getSpeedMph(timeSec) + " mph");
    vehicle = new Truck(3300, vehicleWeightPounds, engineHorsePower);
    System.out.println("Payload=" + 
                          ((Truck)vehicle).getPayload() + " pounds");
    System.out.println("Truck speed (" + timeSec + " sec) = " + 
                     ((Truck)vehicle).getSpeedMph(timeSec) + " mph");
  }
}

正如您所期望的那样,测试类产生相同的值:

为了简化测试代码,我们可以放弃转换,改为写以下内容:

public static void main(String... arg) {
  double timeSec = 10.0;
  int engineHorsePower = 246;
  int vehicleWeightPounds = 4000;
  Car car = new Car(4, vehicleWeightPounds, engineHorsePower);
  System.out.println("Passengers count=" + car.getPassengersCount());
  System.out.println("Car speed (" + timeSec + " sec) = " + 
                                  car.getSpeedMph(timeSec) + " mph");
  Truck truck = 
              new Truck(3300, vehicleWeightPounds, engineHorsePower);
  System.out.println("Payload=" + truck.getPayload() + " pounds");
  System.out.println("Truck speed (" + timeSec + " sec) = " + 
                                truck.getSpeedMph(timeSec) + " mph");
}

此代码产生的速度值保持不变。

然而,有一种更简单的方法可以实现相同的效果。我们可以将getMaxWeightPounds()方法添加到基类和每个子类中。现在Car类将如下所示:

public class Car extends Vehicle {
  private int passengersCount, weightPounds;
  public Car(int passengersCount, int weightPounds, int horsePower){
    super(weightPounds, horsePower);
    this.passengersCount = passengersCount;
    this.weightPounds = weightPounds;
  }
  public int getPassengersCount() { 
    return this.passengersCount;
  }
  public int getMaxWeightPounds() {
    return this.weightPounds + this.passengersCount * 250;
  }
}

现在Truck类的新版本如下所示:

public class Truck extends Vehicle {
  private int payload, weightPounds;
  public Truck(int payloadPounds, int weightPounds, int horsePower) {
    super(weightPounds, horsePower);
    this.payload = payloadPounds;
    this.weightPounds = weightPounds;
  }
  public int getPayload() { return this.payload; }
  public int getMaxWeightPounds() {
    return this.weightPounds + this.payload;
  }
}

我们还需要在基类中添加getMaxWeightPounds()方法,以便用于速度计算:

public abstract class Vehicle {
  private int weightPounds, horsePower;
  public Vehicle(int weightPounds, int horsePower) {
    this.weightPounds = weightPounds;
    this.horsePower = horsePower;
  }
  public abstract int getMaxWeightPounds();
  public double getSpeedMph(double timeSec){
    double v = 2.0 * this.horsePower * 746 * 
                             timeSec * 32.17 / getMaxWeightPounds();
    return Math.round(Math.sqrt(v) * 0.68);
  }
}

Vehicle类添加一个抽象方法getMaxWeightPounds()会使该类成为抽象类。这有一个积极的副作用——它强制在每个子类中实现getMaxWeightPounds()方法。否则,子类将无法实例化,必须声明为抽象类。

测试类保持不变,产生相同的结果:

但是,说实话,我们这样做只是为了演示使用抽象方法和类的一种可能方式。事实上,一个更简单的解决方案是将最大重量作为参数传递给Vehicle基类的构造函数。结果类将如下所示:

public class Car extends Vehicle {
  private int passengersCount;
  public Car(int passengersCount, int weightPounds, int horsepower){
    super(weightPounds + passengersCount * 250, horsePower);
    this.passengersCount = passengersCount;
  }
  public int getPassengersCount() { 
    return this.passengersCount; }
}

我们将乘客的重量添加到我们传递给超类构造函数的值中;这是这个子类中唯一的变化。Truck类中也有类似的变化:

public class Truck extends Vehicle {
  private int payload;
  public Truck(int payloadPounds, int weightPounds, int horsePower) {
    super(weightPounds + payloadPounds, horsePower);
    this.payload = payloadPounds;
  }
  public int getPayload() { return this.payload; }
}

Vehicle基类与原始的一样:

public class Vehicle {
  private int weightPounds, horsePower;
  public Vehicle(int weightPounds, int horsePower) {
    this.weightPounds = weightPounds;
    this.horsePower = horsePower;
  }
  public double getSpeedMph(double timeSec){
    double v = 2.0 * this.horsePower * 746;
    v = v * timeSec * 32.174 / this.weightPounds;
    return Math.round(Math.sqrt(v) * 0.68);
  }
}

测试类不会改变,并且产生相同的结果:

这个最后的版本——将最大重量传递给基类的构造函数——现在将成为进一步代码演示的起点。

聚合使设计更具可扩展性

在上面的例子中,速度模型是在Vehicle类的getSpeedMph(double timeSec)方法中实现的。如果我们需要使用不同的速度模型(例如包含更多输入参数并且更适合特定驾驶条件),我们需要更改Vehicle类或创建一个新的子类来覆盖该方法。在需要尝试几十甚至数百种不同模型的情况下,这种方法变得不可行。

此外,在现实生活中,基于机器学习和其他先进技术的建模变得如此复杂和专业化,以至于汽车加速的建模通常是由不同的团队完成的,而不是组装车辆模型的团队。

为了避免子类的激增和车辆构建者与速度模型开发者之间的代码合并冲突,我们可以使用聚合创建一个更具可扩展性的设计。

聚合是一种面向对象设计原则,用于使用不属于继承层次结构的类的行为来实现必要的功能。该行为可以独立于聚合功能存在。

我们可以将速度计算封装在SpeedModel类的getSpeedMph(double timeSec)方法中:

public class SpeedModel{
  private Properties conditions;
  public SpeedModel(Properties drivingConditions){
    this.drivingConditions = drivingConditions;
  }
  public double getSpeedMph(double timeSec, int weightPounds,
                                               int horsePower){
    String road = 
         drivingConditions.getProperty("roadCondition","Dry");
    String tire = 
         drivingConditions.getProperty("tireCondition","New");
    double v = 2.0 * horsePower * 746 * timeSec * 
                                         32.17 / weightPounds;
    return Math.round(Math.sqrt(v)*0.68)-road.equals("Dry")? 2 : 5) 
                                       -(tire.equals("New")? 0 : 5);
  }
}

可以创建这个类的对象,然后将其设置为Vehicle类字段的值:

public class Vehicle {
   private SpeedModel speedModel;       
   private int weightPounds, horsePower;
   public Vehicle(int weightPounds, int horsePower) {
      this.weightPounds = weightPounds;
      this.horsePower = horsePower;
   }
   public void setSpeedModel(SpeedModel speedModel){
      this.speedModel = speedModel;
   }
   public double getSpeedMph(double timeSec){
      return this.speedModel.getSpeedMph(timeSec,
                       this.weightPounds, this.horsePower);
   }
}

测试类的更改如下:

public static void main(String... arg) {
  double timeSec = 10.0;
  int horsePower = 246;
  int vehicleWeight = 4000;
  Properties drivingConditions = new Properties();
  drivingConditions.put("roadCondition", "Wet");
  drivingConditions.put("tireCondition", "New");
  SpeedModel speedModel = new SpeedModel(drivingConditions);
  Car car = new Car(4, vehicleWeight, horsePower);
  car.setSpeedModel(speedModel);
  System.out.println("Car speed (" + timeSec + " sec) = " + 
                         car.getSpeedMph(timeSec) + " mph");
}

上述代码的结果如下:

我们将速度计算功能隔离到一个单独的类中,现在可以修改或扩展它,而不需要改变Vehicle继承层次结构中的任何类。这就是聚合设计原则允许您在不改变实现的情况下改变行为的方式。

在下一个示例中,我们将向您展示接口的面向对象编程概念如何释放出更多聚合和多态的力量,使设计更简单,甚至更具表现力。

编码到接口

在这个示例中,您将学习面向对象编程概念中的最后一个接口,并进一步练习聚合和多态的使用,以及内部类和继承。

准备工作

接口定义了一个类中可以期望看到的方法的签名。它是对客户端可访问的功能的公共界面,因此通常被称为应用程序编程接口API)。它支持多态和聚合,并促进更灵活和可扩展的设计。

接口是隐式抽象的,这意味着它不能被实例化。不能仅基于接口创建对象,而不实现它。它仅用于包含抽象方法(无方法体)。但自 Java 8 以来,可以向接口添加默认和私有方法,这是我们将在以下示例中讨论的功能。

每个接口可以扩展多个其他接口,并且类继承类似,继承所有扩展接口的默认和抽象方法。静态成员不能被继承,因为它们属于特定接口。

如何做...

  1. 创建描述 API 的接口:
public interface SpeedModel {
   double getSpeedMph(double timeSec, int weightPounds, 
                                            int horsePower);
 }
 public interface Vehicle {
   void setSpeedModel(SpeedModel speedModel);
   double getSpeedMph(double timeSec);
 }
 public interface Car extends Vehicle {
   int getPassengersCount();
 }
 public interface Truck extends Vehicle {
   int getPayloadPounds();
 }
  1. 使用工厂,这些工厂是生成实现特定接口的对象的类。工厂隐藏了实现的细节,因此客户端只处理接口。当实例创建需要复杂的过程和/或大量代码重复时,这是特别有帮助的。在我们的情况下,有一个FactoryVehicle类是有意义的,它创建实现VehicleCarTruck接口的类的对象。我们还将创建FactorySpeedModel类,它生成实现SpeedModel接口的类的对象。这样的 API 允许我们编写以下代码:
public static void main(String... arg) {
   double timeSec = 10.0;
   int horsePower = 246;
   int vehicleWeight = 4000;
   Properties drivingConditions = new Properties();
   drivingConditions.put("roadCondition", "Wet");
   drivingConditions.put("tireCondition", "New");
   SpeedModel speedModel  = FactorySpeedModel.
                 generateSpeedModel(drivingConditions);
   Car car = FactoryVehicle.
                buildCar(4, vehicleWeight, horsePower);
   car.setSpeedModel(speedModel);
   System.out.println("Car speed (" + timeSec + " sec) = " 
                      + car.getSpeedMph(timeSec) + " mph");
}
  1. 请注意,代码行为与先前示例中的相同:

然而,设计更具可扩展性。

它是如何工作的...

我们已经看到了SpeedModel接口的一个可能的实现。这是另一种通过在FactorySpeedModel类内聚SpeedModel类型对象来实现的方法:

public class FactorySpeedModel {
  public static SpeedModel generateSpeedModel(
  Properties drivingConditions){
    //if drivingConditions includes "roadCondition"="Wet"
    return new SpeedModelWet(...);
    //if drivingConditions includes "roadCondition"="Dry"
    return new SpeedModelDry(...);
  }
  private class SpeedModelWet implements SpeedModel{
    public double getSpeedMph(double timeSec, int weightPounds, 
                                                     int horsePower){
       //method code goes here
    }
  }
  private class SpeedModelDry implements SpeedModel{
    public double getSpeedMph(double timeSec, int weightPounds, 
                                                     int horsePower){
      //method code goes here
    }
  }
}

我们将注释作为伪代码,并使用...符号代替实际代码,以便简洁。

如您所见,工厂类可能隐藏许多不同的私有类,每个类都包含特定驾驶条件的专门模型。每个模型产生不同的结果。

FactoryVehicle类的实现可能如下所示:

public class FactoryVehicle {
  public static Car buildCar(int passengersCount, 
                               int weightPounds, int horsePower){
    return new CarImpl(passengersCount, weightPounds,horsePower);
  }
  public static Truck buildTruck(int payloadPounds, 
                               int weightPounds, int horsePower){
    return new TruckImpl(payloadPounds, weightPounds,horsePower);
  }
}

CarImpl私有嵌套类在FactoryVehicle类内部可能如下所示:

  private static class CarImpl extends VehicleImpl implements Car {
    private int passengersCount;
    private CarImpl(int passengersCount, int weightPounds,
                                                 int horsePower){
      super(weightPounds + passengersCount * 250, horsePower);
      this.passengersCount = passengersCount;
    }
    public int getPassengersCount() { 
      return this.passengersCount;
    }
  }

同样,TruckImpl类可以是FactoryImpl类的私有嵌套类:

  private static class TruckImpl extends VehicleImpl implements Truck {
    private int payloadPounds;
    private TruckImpl(int payloadPounds, int weightPounds, 
                                                     int horsePower){
      super(weightPounds+payloadPounds, horsePower);
      this.payloadPounds = payloadPounds;
    }
    public int getPayloadPounds(){ return payloadPounds; }
  }

我们也可以将VehicleImpl类作为FactoryVehicle类的私有内部类放置,这样CarImplTruckImpl类就可以访问它,但是FactoryVehicle之外的任何其他类都不能访问它:

  private static abstract class VehicleImpl implements Vehicle {
    private SpeedModel speedModel;
    private int weightPounds, horsePower;
    private VehicleImpl(int weightPounds, int horsePower){
      this.weightPounds = weightPounds;
      this.horsePower = horsePower;
    }
    public void setSpeedModel(SpeedModel speedModel){ 
      this.speedModel = speedModel; 
    }
    public double getSpeedMph(double timeSec){
      return this.speedModel.getSpeedMph(timeSec, weightPounds, 
                                                    horsePower);
    }
  }

如您所见,接口描述了如何调用对象行为,而工厂可以为不同的请求生成不同的实现,而不改变客户端应用程序的代码。

还有更多...

让我们尝试建模一个乘员舱——一个具有多个乘客座位的卡车,它结合了汽车和卡车的特性。Java 不允许多重继承。这是另一个接口发挥作用的案例。

CrewCab类可能如下所示:

public class CrewCab extends VehicleImpl implements Car, Truck {
  private int payloadPounds;
  private int passengersCount;
  private CrewCabImpl(int passengersCount, int payloadPounds,
                             int weightPounds, int horsePower) {
     super(weightPounds + payloadPounds + passengersCount * 250, 
                                                     horsePower);
     this.payloadPounds = payloadPounds;
     this. passengersCount = passengersCount;
  }
  public int getPayloadPounds(){ return payloadPounds; }
  public int getPassengersCount() { 
     return this.passengersCount;
  }
}

这个类同时实现了CarTruck接口,并将车辆、货物和乘客及其行李的总重量传递给基类构造函数。

我们还可以向FactoryVehicle添加以下方法:

public static Vehicle buildCrewCab(int passengersCount, 
                      int payload, int weightPounds, int horsePower){
  return new CrewCabImpl(passengersCount, payload, 
                                           weightPounds, horsePower);
}

CrewCab对象的双重性质可以在以下测试中得到证明:

public static void main(String... arg) {
  double timeSec = 10.0;
  int horsePower = 246;
  int vehicleWeight = 4000;
  Properties drivingConditions = new Properties();
  drivingConditions.put("roadCondition", "Wet");
  drivingConditions.put("tireCondition", "New");
  SpeedModel speedModel = 
      FactorySpeedModel.generateSpeedModel(drivingConditions);
  Vehicle vehicle = FactoryVehicle.
             buildCrewCab(4, 3300, vehicleWeight, horsePower);
  vehicle.setSpeedModel(speedModel);
  System.out.println("Payload = " +
            ((Truck)vehicle).getPayloadPounds()) + " pounds");
  System.out.println("Passengers count = " + 
                         ((Car)vehicle).getPassengersCount());
  System.out.println("Crew cab speed (" + timeSec + " sec) = "  
                     + vehicle.getSpeedMph(timeSec) + " mph");
}

正如您所看到的,我们可以将CrewCub类的对象转换为它实现的每个接口。如果我们运行这个程序,结果将如下所示:

创建具有默认和静态方法的接口

在这个示例中,您将了解到 Java 8 中首次引入的两个新功能——接口中的默认和静态方法。

准备就绪

接口中的默认方法允许我们添加一个新的方法签名,而不需要改变在添加新方法签名之前已经实现了该接口的类。该方法被称为default,因为它在该方法未被类实现时提供功能。然而,如果类实现了它,接口的默认实现将被忽略并被类实现覆盖。

接口中的静态方法可以提供与类中的静态方法相同的功能。类似于类静态方法可以在不实例化类的情况下调用一样,接口静态方法也可以使用点运算符应用于接口来调用,SomeInterface.someStaticMethod()

接口的静态方法不能被实现该接口的类覆盖,也不能隐藏任何类的静态方法,包括实现该接口的类。

例如,让我们为我们已经在示例中使用的系统添加一些功能。到目前为止,我们已经创建了一个了不起的软件,可以计算车辆的速度。如果系统变得受欢迎(应该是这样),我们希望它对更喜欢使用公制单位的读者更友好,而不是我们在速度计算中使用的英里和磅。在我们的速度计算软件变得受欢迎之后,为了满足这样的需求,我们决定向CarTruck接口添加更多方法,但我们不想破坏现有的实现。

默认接口方法正是为这种情况而引入的。使用它,我们可以发布CarTruck接口的新版本,而无需协调与现有实现的相应修改,即CarImplTruckImplFactoryVehicle类。

如何做...

例如,我们将更改Truck接口。Car接口可以以类似的方式修改:

  1. 通过添加一个新的默认方法来增强Truck接口,该方法返回卡车的载重量(以千克为单位)。您可以在不强制更改实现Truck接口的TruckImpl类的情况下完成这一点——通过向Truck接口添加一个新的默认方法:
      public interface Truck extends Vehicle {
          int getPayloadPounds();
          default int getPayloadKg(){
            return (int) Math.round(0.454 * getPayloadPounds());
          }
      }

注意新的getPayloadKg()方法如何使用现有的getPayloadPounds()方法,就好像后者也是在接口内实现的一样,尽管实际上它是在实现Truck接口的类中实现的。魔术发生在运行时,当这个方法动态绑定到实现该接口的类的实例时。

我们无法将getPayloadKg()方法设为静态,因为它无法访问非静态的getPayloadPounds()方法,我们必须使用default关键字,因为只有接口的默认或静态方法才能有方法体。

  1. 编写使用新方法的客户端代码:
      public static void main(String... arg) {
         Truck truck = FactoryVehicle.buildTruck(3300, 4000, 246);
         System.out.println("Payload in pounds: " + 
                                        truck.getPayloadPounds());
         System.out.println("Payload in kg: " + 
                                            truck.getPayloadKg());
      }
  1. 运行上述程序并检查输出:

  1. 请注意,即使不改变实现它的类,新方法也可以工作。

  2. 当您决定改进TruckImpl类的实现时,您可以通过添加相应的方法来实现,例如:

       class TruckImpl extends VehicleImpl implements Truck {
          private int payloadPounds;
          private TruckImpl(int payloadPounds, int weightPounds,
                                                int horsePower) {
            super(weightPounds + payloadPounds, horsePower);
            this.payloadPounds = payloadPounds;
          }
          public int getPayloadPounds(){ return payloadPounds; }
          public int getPayloadKg(){ return -2; }
       }

我们已经实现了getPyloadKg()方法,使其为return -2,以便明确使用了哪种实现。

  1. 运行相同的演示程序。结果将如下所示:

正如您所看到的,这次在TruckImpl类中使用了方法实现。它已覆盖了Truck接口中的默认实现。

  1. 增强Truck接口的功能,使其能够以千克为单位输入有效载荷,而不改变FactoryVehicleTruck接口的实现。此外,我们不希望添加一个 setter 方法。在所有这些限制下,我们唯一的选择是在Truck接口中添加convertKgToPounds(int kgs)方法,并且它必须是静态的,因为我们将在实现Truck接口的对象构造之前使用它:
      public interface Truck extends Vehicle {
          int getPayloadPounds();
          default int getPayloadKg(){
            return (int) Math.round(0.454 * getPayloadPounds());
          }
          static int convertKgToPounds(int kgs){
            return (int) Math.round(2.205 * kgs);
          }
       }

工作原理...

现在,喜欢使用公制单位的人可以利用新的方法:

public static void main(String... arg) {
  int horsePower = 246;
  int payload = Truck.convertKgToPounds(1500);
  int vehicleWeight = Truck.convertKgToPounds(1800);
  Truck truck = FactoryVehicle.
           buildTruck(payload, vehicleWeight, horsePower);
  System.out.println("Payload in pounds: " + 
                                truck.getPayloadPounds());
  int kg = truck.getPayloadKg();
  System.out.println("Payload converted to kg: " + kg);
  System.out.println("Payload converted back to pounds: " + 
                              Truck.convertKgToPounds(kg));
}

结果将如下所示:

1,502 的值接近原始的 1,500,而 3,308 接近 3,312。差异是由转换过程中的近似误差引起的。

创建具有私有方法的接口

在本教程中,您将了解 Java 9 中引入的新功能——私有接口方法,它有两种类型:静态和非静态。

准备就绪

私有接口方法必须有实现(具有代码的主体)。同一接口的其他方法未使用的私有接口方法是没有意义的。私有方法的目的是包含在同一接口中具有主体的两个或多个方法之间的常见功能,或者将代码部分隔离在单独的方法中,以获得更好的结构和可读性。私有接口方法不能被覆盖,既不能被任何其他接口的方法覆盖,也不能被实现接口的类中的方法覆盖。

非静态私有接口方法只能被同一接口的非静态方法访问。静态私有接口方法可以被同一接口的非静态和静态方法访问。

如何做...

  1. 添加getWeightKg(int pounds)方法的实现:
     public interface Truck extends Vehicle {
         int getPayloadPounds();
         default int getPayloadKg(){
            return (int) Math.round(0.454 * getPayloadPounds());
         }
         static int convertKgToPounds(int kilograms){
            return (int) Math.round(2.205 * kilograms);
         }
         default int getWeightKg(int pounds){
            return (int) Math.round(0.454 * pounds);
         }
     }
  1. 通过使用私有接口方法删除冗余代码:
    public interface Truck extends Vehicle {
        int getPayloadPounds();
        default int getPayloadKg(int pounds){
            return convertPoundsToKg(pounds);
        }
        static int convertKgToPounds(int kilograms){
            return (int) Math.round(2.205 * kilograms);
        }
        default int getWeightKg(int pounds){
            return convertPoundsToKg(pounds);
        }
        private int convertPoundsToKg(int pounds){
            return (int) Math.round(0.454 * pounds);
        }
    }

工作原理...

以下代码演示了新的添加内容:

public static void main(String... arg) {
  int horsePower = 246;
  int payload = Truck.convertKgToPounds(1500);
  int vehicleWeight = Truck.convertKgToPounds(1800);
  Truck truck = 
      FactoryVehicle.buildTruck(payload, vehicleWeight, horsePower);
  System.out.println("Weight in pounds: " + vehicleWeight);
  int kg = truck.getWeightKg(vehicleWeight);
  System.out.println("Weight converted to kg: " + kg);
  System.out.println("Weight converted back to pounds: " + 
                                       Truck.convertKgToPounds(kg));
}

测试结果不会改变:

还有更多...

由于getWeightKg(int pounds)方法接受输入参数,方法名称可能会误导,因为它没有捕捉输入参数的重量单位。我们可以尝试将其命名为getWeightKgFromPounds(int pounds),但这并不会使方法功能更清晰。在意识到这一点后,我们决定将convertPoundsToKg(int pounds)方法设为公共方法,并完全删除getWeightKg(int pounds)方法。由于convertPoundsToKg(int pounds)方法不需要访问对象字段,它也可以是静态的:

public interface Truck extends Vehicle {
  int getPayloadPounds();
  default int getPayloadKg(int pounds){
    return convertPoundsToKg(pounds);
  }
  static int convertKgToPounds(int kilograms){
    return (int) Math.round(2.205 * kilograms);
  }
  static int convertPoundsToKg(int pounds){
    return (int) Math.round(0.454 * pounds);
  }
}

仍然可以将英镑转换为千克,而且由于两种转换方法都是静态的,我们不需要创建实现Truck接口的类的实例来进行转换:

public static void main(String... arg) {
  int payload = Truck.convertKgToPounds(1500);
  int vehicleWeight = Truck.convertKgToPounds(1800);
  System.out.println("Weight in pounds: " + vehicleWeight);
  int kg = Truck.convertPoundsToKg(vehicleWeight);
  System.out.println("Weight converted to kg: " + kg);
  System.out.println("Weight converted back to pounds: " + 
                                     Truck.convertKgToPounds(kg));
}

结果不会改变:

使用 Optional 更好地处理空值

在本教程中,您将学习如何使用java.util.Optional类来表示可选值,而不是使用null引用。它是在 Java 8 中引入的,并在 Java 9 中进一步增强,增加了三种方法:or()ifPresentOrElse()stream()。我们将演示它们全部。

准备就绪

Optional类是一个围绕值的包装器,可以是null或任何类型的值。它旨在帮助避免可怕的NullPointerException。但是,到目前为止,引入Optional只在流和函数式编程领域有所帮助。

创建Optional类的愿景是在Optional对象上调用isPresent()方法,然后仅在isPresent()方法返回true时应用get()方法(获取包含的值)。不幸的是,当无法保证Optional对象本身的引用不是null时,需要检查它以避免NullPointerException。如果是这样,那么使用Optional的价值就会减少,因为即使写入更少的代码,我们也可以检查值本身是否为null并避免包装在Optional中?让我们编写代码来说明我们所说的。

假设我们想编写一个方法来检查彩票结果,如果你和朋友一起买的彩票中奖了,计算你的 50%份额。传统的做法是:

void checkResultInt(int lotteryPrize){
    if(lotteryPrize <= 0){
        System.out.println("We've lost again...");
    } else {
        System.out.println("We've won! Your half is " + 
                     Math.round(((double)lotteryPrize)/2) + "!");
    }
}

但是,为了演示如何使用Optional,我们将假设结果是Integer类型。然后,我们还需要检查null,如果我们不能确定传入的值不可能是null

void checkResultInt(Integer lotteryPrize){
    if(lotteryPrize == null || lotteryPrize <= 0){
        System.out.println("We've lost again...");
    } else {
        System.out.println("We've won! Your half is " + 
                    Math.round(((double)lotteryPrize)/2) + "!");
    }
}

使用Optional类并不能帮助避免对null的检查。它甚至需要添加额外的检查isPresent(),以避免在获取值时出现NullPointerException

void checkResultOpt(Optional<Integer> lotteryPrize){
    if(lotteryPrize == null || !lotteryPrize.isPresent() 
                                        || lotteryPrize.get() <= 0){
        System.out.println("We lost again...");
    } else {
        System.out.println("We've won! Your half is " + 
                   Math.round(((double)lotteryPrize.get())/2) + "!");
    }
}

显然,先前使用Optional并没有帮助改进代码或使编码更容易。在 Lambda 表达式和流管道中使用Optional具有更大的潜力,因为Optional对象提供了可以通过点运算符调用的方法,并且可以插入到流畅式处理代码中。

如何做...

  1. 使用已经演示过的任何方法创建一个Optional对象,如下所示:
Optional<Integer> prize1 = Optional.empty();
System.out.println(prize1.isPresent()); //prints: false
System.out.println(prize1);   //prints: Optional.empty

Optional<Integer> prize2 = Optional.of(1000000);
System.out.println(prize2.isPresent()); //prints: true
System.out.println(prize2);  //prints: Optional[1000000]

//Optional<Integer> prize = Optional.of(null); 
                                  //NullPointerException

Optional<Integer> prize3 = Optional.ofNullable(null);
System.out.println(prize3.isPresent());  //prints: false
System.out.println(prize3);     //prints: Optional.empty

请注意,可以使用ofNullable()方法将null值包装在Optional对象中。

  1. 可以使用equals()方法比较两个Optional对象,该方法通过值进行比较:
Optional<Integer> prize1 = Optional.empty();
System.out.println(prize1.equals(prize1)); //prints: true

Optional<Integer> prize2 = Optional.of(1000000);
System.out.println(prize1.equals(prize2)); //prints: false

Optional<Integer> prize3 = Optional.ofNullable(null);
System.out.println(prize1.equals(prize3)); //prints: true

Optional<Integer> prize4 = Optional.of(1000000);
System.out.println(prize2.equals(prize4)); //prints: true
System.out.println(prize2 == prize4); //prints: false

Optional<Integer> prize5 = Optional.of(10);
System.out.println(prize2.equals(prize5)); //prints: false

Optional<String> congrats1 = Optional.empty();
System.out.println(prize1.equals(congrats1));//prints: true

Optional<String> congrats2 = Optional.of("Happy for you!");
System.out.println(prize1.equals(congrats2));//prints: false

请注意,空的Optional对象等于包装null值的对象(上述代码中的prize1prize3对象)。上述代码中的prize2prize4对象相等,因为它们包装相同的值,尽管它们是不同的对象,引用不匹配(prize2 != prize4)。还要注意,包装不同类型的空对象是相等的(prize1.equals(congrats1)),这意味着Optional类的equals()方法不比较值类型。

  1. 使用Optional类的or(Suppier<Optional<T>> supplier)方法可靠地从Optional对象中返回非空值。如果对象为空并包含null,则它将返回由提供的Supplier函数生成的Optional对象中的另一个值。

例如,如果Optional<Integer> lotteryPrize对象可能包含null值,则以下结构将在遇到null值时每次返回零:

       int prize = lotteryPrize.or(() -> Optional.of(0)).get();

  1. 使用ifPresent(Consumer<T> consumer)方法来忽略null值,并使用提供的Consumer<T>函数处理非空值。例如,这是processIfPresent(Optional<Integer>)方法,它处理Optional<Integer> lotteryPrize对象:
void processIfPresent(Optional<Integer> lotteryPrize){
   lotteryPrize.ifPresent(prize -> {
      if(prize <= 0){
          System.out.println("We've lost again...");
      } else {
          System.out.println("We've won! Your half is " + 
                    Math.round(((double)prize)/2) + "!");
     }
});

我们可以通过创建checkResultAndShare(int prize)方法简化上述代码:

void checkResultAndShare(int prize){
    if(prize <= 0){
        System.out.println("We've lost again...");
    } else {
        System.out.println("We've won! Your half is " + 
                   Math.round(((double)prize)/2) + "!");
    }
}

现在,processIfPresent()方法看起来简单得多:

void processIfPresent(Optional<Integer> lotteryPrize){
    lotteryPrize.ifPresent(prize -> checkResultAndShare(prize));
}
  1. 如果您不想忽略null值并且也要处理它,可以使用ifPresentOrElse(Consumer<T> consumer, Runnable processEmpty)方法将Consumer<T>函数应用于非空值,并使用Runnable函数接口来处理null值:
void processIfPresentOrElse(Optional<Integer> lotteryPrize){
   Consumer<Integer> weWon = 
                       prize -> checkResultAndShare(prize);
   Runnable weLost = 
           () -> System.out.println("We've lost again...");
   lotteryPrize.ifPresentOrElse(weWon, weLost);
}

正如您所见,我们已经重用了刚刚创建的checkResultAndShare(int prize)方法。

  1. 使用orElseGet(Supplier<T> supplier)方法允许我们用由提供的Supplier<T>函数产生的值来替换Optional对象中的空值或null值:
void processOrGet(Optional<Integer> lotteryPrize){
   int prize = lotteryPrize.orElseGet(() -> 42);
   lotteryPrize.ifPresentOrElse(p -> checkResultAndShare(p),
      () -> System.out.println("Better " + prize 
                                     + " than nothing..."));
 }
  1. 如果需要在Optional对象为空或包含null值的情况下抛出异常,请使用orElseThrow()方法:
void processOrThrow(Optional<Integer> lotteryPrize){
   int prize = lotteryPrize.orElseThrow();
   checkResultAndShare(prize);
}

orElseThrow()方法的重载版本允许我们指定异常和当Optional对象中包含的值为null时要抛出的消息:

void processOrThrow(Optional<Integer> lotteryPrize){
    int prize = lotteryPrize.orElseThrow(() -> 
           new RuntimeException("We've lost again..."));
    checkResultAndShare(prize);
}
  1. 使用filter()map()flatMap()方法来处理流中的Optional对象:
void useFilter(List<Optional<Integer>> list){
   list.stream().filter(opt -> opt.isPresent())
         .forEach(opt -> checkResultAndShare(opt.get()));
}
void useMap(List<Optional<Integer>> list){
   list.stream().map(opt -> opt.or(() -> Optional.of(0)))
         .forEach(opt -> checkResultAndShare(opt.get()));
}
void useFlatMap(List<Optional<Integer>> list){
   list.stream().flatMap(opt -> 
           List.of(opt.or(()->Optional.of(0))).stream())
        .forEach(opt -> checkResultAndShare(opt.get()));
}

在前面的代码中,useFilter()方法只处理那些非空数值的流元素。useMap()方法处理所有流元素,但是用没有值的Optional对象或者用包装了null值的Optional对象替换它们。最后一个方法使用了flatMap(),它需要从提供的函数返回一个流。在这方面,我们的示例是相当无用的,因为我们传递给flatMap()参数的函数产生了一个对象的流,所以在这里使用map()(就像前面的useMap()方法中一样)是一个更好的解决方案。我们只是为了演示flatMap()方法如何插入到流管道中才这样做的。

它是如何工作的...

以下代码演示了所描述的Optional类的功能。useFlatMap()方法接受一个Optional对象列表,创建一个流,并处理每个发出的元素:

void useFlatMap(List<Optional<Integer>> list){
    Function<Optional<Integer>, 
      Stream<Optional<Integer>>> tryUntilWin = opt -> {
        List<Optional<Integer>> opts = new ArrayList<>();
        if(opt.isPresent()){
            opts.add(opt);
        } else {
            int prize = 0;
            while(prize == 0){
                double d = Math.random() - 0.8;
                prize = d > 0 ? (int)(1000000 * d) : 0;
                opts.add(Optional.of(prize));
            }
        }
        return opts.stream();
    };
    list.stream().flatMap(tryUntilWin)
        .forEach(opt -> checkResultAndShare(opt.get()));
}

原始列表的每个元素首先作为输入进入flatMap()方法,然后作为tryUntilWin函数的输入。这个函数首先检查Optional对象的值是否存在。如果是,Optional对象将作为流的单个元素发出,并由checkResultAndShare()方法处理。但是如果tryUntilWin函数确定Optional对象中没有值或者值为null,它会在-0.80.2之间生成一个随机双精度数。如果值为负数,就会向结果列表中添加一个值为零的Optional对象,并生成一个新的随机数。但如果生成的数是正数,它将用于奖金值的计算,并添加到包装在Optional对象中的结果列表中。Optional对象的结果列表然后作为流返回,并且流的每个元素都由checkResultAndShare()方法处理。

现在,让我们对以下列表运行前面的方法:

List<Optional<Integer>> list = List.of(Optional.empty(), 
                                       Optional.ofNullable(null), 
                                       Optional.of(100000));
useFlatMap(list);

结果将如下所示:

如您所见,当第一个列表元素Optional.empty()被处理时,tryUntilWin函数在第三次尝试中成功获得了一个正的奖金值。第二个Optional.ofNullable(null)对象导致了两次尝试,直到tryUntilWin函数成功。最后一个对象成功通过,并奖励您和您的朋友各 50000。

还有更多...

Optional类的对象不可序列化,因此不能用作对象的字段。这是Optional类的设计者打算在无状态过程中使用的另一个指示。

它使流处理管道更加简洁和表达,专注于实际值而不是检查流中是否有空元素。

使用实用类 Objects

在这个配方中,您将学习java.util.Objects实用类如何允许更好地处理与对象比较、计算哈希值和检查null相关的功能。这是早就该有的功能,因为程序员们一遍又一遍地编写相同的代码来检查对象是否为null

准备工作

Objects类只有 17 种方法,全部都是静态的。为了更好地概述,我们将它们组织成了七个组:

  • compare(): 使用提供的Comparator比较两个对象的方法

  • toString(): 将Object转换为String值的两种方法

  • checkIndex(): 三种允许我们检查集合或数组的索引和长度是否兼容的方法

  • requireNonNull(): 如果提供的对象为null,则五种方法抛出异常

  • hash(), hashCode(): 计算单个对象或对象数组的哈希值的两种方法

  • isNull()nonNull(): 包装obj == nullobj != null表达式的两种方法

  • equals(), deepEquals(): 比较两个可以为 null 或数组的对象的两种方法

我们将按照前述顺序编写使用这些方法的代码。

如何做...

  1. int compare(T a, T b, Comparator<T> c)方法使用提供的比较器来比较两个对象:
  • 当对象相等时返回 0

  • 当第一个对象小于第二个对象时返回负数

  • 否则返回正数

int compare(T a, T b, Comparator<T> c)方法的非零返回值取决于实现。对于String,根据它们的排序位置定义较小和较大(较小的放在有序列表的前面),返回值是第一个和第二个参数在列表中的位置之间的差异,根据提供的比较器排序:

int res =
      Objects.compare("a", "c", Comparator.naturalOrder());
System.out.println(res);       //prints: -2
res = Objects.compare("a", "a", Comparator.naturalOrder());
System.out.println(res);       //prints: 0
res = Objects.compare("c", "a", Comparator.naturalOrder());
System.out.println(res);       //prints: 2
res = Objects.compare("c", "a", Comparator.reverseOrder());
System.out.println(res);       //prints: -2

另一方面,Integer值在值不相等时返回-11

res = Objects.compare(3, 5, Comparator.naturalOrder());
System.out.println(res);       //prints: -1
res = Objects.compare(3, 3, Comparator.naturalOrder());
System.out.println(res);       //prints: 0
res = Objects.compare(5, 3, Comparator.naturalOrder());
System.out.println(res);       //prints: 1
res = Objects.compare(5, 3, Comparator.reverseOrder());
System.out.println(res);       //prints: -1
res = Objects.compare("5", "3", Comparator.reverseOrder());
System.out.println(res);       //prints: -2

请注意,在前面代码块的最后一行中,当我们将数字作为String文字进行比较时,结果会发生变化。

当两个对象都为null时,compare()方法将认为它们相等:

res = Objects.compare(null,null,Comparator.naturalOrder());
System.out.println(res);       //prints: 0

但是当其中一个对象为 null 时,会抛出NullPointerException

//Objects.compare(null, "c", Comparator.naturalOrder());   
//Objects.compare("a", null, Comparator.naturalOrder());   

如果需要将对象与 null 进行比较,最好使用org.apache.commons.lang3.ObjectUtils.compare(T o1, T o2)

  1. null时,toString(Object obj)方法很有用:
  • String toString(Object obj): 当第一个参数不为null时,返回调用toString()的结果,当第一个参数值为null时,返回null

  • String toString(Object obj, String nullDefault): 当第一个参数不为null时,返回调用toString()的结果,当第一个参数值为null时,返回第二个参数值nullDefault

toString(Object obj)方法的使用很简单:

System.out.println(Objects.toString("a")); //prints: a
System.out.println(Objects.toString(null)); //prints: null
System.out.println(Objects.toString("a", "b")); //prints: a
System.out.println(Objects.toString(null, "b"));//prints: b
  1. checkIndex()重载方法检查集合或数组的索引和长度是否兼容:
  • int checkIndex(int index, int length): 如果提供的index大于length - 1,则抛出IndexOutOfBoundsException,例如:
List<Integer> list = List.of(1, 2);
try {
   Objects.checkIndex(3, list.size());
} catch (IndexOutOfBoundsException ex){
   System.out.println(ex.getMessage()); 
       //prints: Index 3 out-of-bounds for length 2
}
    • int checkFromIndexSize(int fromIndex, int size, int length): 如果提供的index + size大于length - 1,则抛出IndexOutOfBoundsException,例如:
List<Integer> list = List.of(1, 2);
try {
   Objects.checkFromIndexSize(1, 3, list.size());
} catch (IndexOutOfBoundsException ex){
   System.out.println(ex.getMessage());
//prints:Range [1, 1 + 3) out-of-bounds for length 2
}
    • int checkFromToIndex(int fromIndex, int toIndex, int length): 如果提供的fromIndex大于toIndex,或者toIndex大于length - 1,则抛出IndexOutOfBoundsException,例如:
List<Integer> list = List.of(1, 2);
try {
   Objects.checkFromToIndex(1, 3, list.size());
} catch (IndexOutOfBoundsException ex){
   System.out.println(ex.getMessage()); 
   //prints:Range [1, 3) out-of-bounds for length 2
}
  1. requireNonNull()组的五种方法检查第一个参数obj的值。如果值为null,它们要么抛出NullPointerException,要么返回提供的默认值:
  • T requireNonNull(T obj): 如果参数为null,则抛出没有消息的NullPointerException,例如:
String obj = null;
try {
  Objects.requireNonNull(obj);
} catch (NullPointerException ex){
  System.out.println(ex.getMessage());//prints: null
}
    • T requireNonNull(T obj, String message): 如果第一个参数为null,则抛出带有提供的消息的NullPointerException,例如:
String obj = null;
try {
  Objects.requireNonNull(obj,  
                          "Parameter 'obj' is null");
} catch (NullPointerException ex){
  System.out.println(ex.getMessage()); 
                 //prints: Parameter 'obj' is null
}
    • T requireNonNull(T obj, Supplier<String> messageSupplier): 如果第一个参数为null,则返回由提供的函数生成的消息,如果生成的消息或函数本身为null,则抛出NullPointerException,例如:
String obj = null;
Supplier<String> supplier = () -> "Message";
try {
  Objects.requireNonNull(obj, supplier);
} catch (NullPointerException ex){
  System.out.println(ex.getMessage()); 
                         //prints: Message
}
    • T requireNonNullElse(T obj, T defaultObj): 返回第一个参数(如果它不是 null),第二个参数(如果它不是 null),抛出NullPointerException(如果两个参数都是null),例如:
String object = null;
System.out.println(Objects
          .requireNonNullElse(obj, "Default value")); 
                          //prints: Default value
    • T requireNonNullElseGet(T obj, Supplier<T> supplier): 返回第一个参数(如果它不是 null),由提供的供应商函数产生的对象(如果它不是 null 且supplier.get()不是 null),抛出NullPointerException(如果两个参数都是null或第一个参数和 supplier.get()都是null),例如:
Integer obj = null;
Supplier<Integer> supplier = () -> 42;
try {
    System.out.println(Objects
              .requireNonNullElseGet(obj, supplier));
} catch (NullPointerException ex){
    System.out.println(ex.getMessage()); //prints: 42
} 
  1. hash()hashCode()方法通常用于覆盖默认的hashCode()实现:
  • int hashCode(Object value): 为单个对象计算哈希值,例如:
System.out.println(Objects.hashCode(null)); 
                                       //prints: 0
System.out.println(Objects.hashCode("abc")); 
                                   //prints: 96354 
  • int hash(Object... values): 为对象数组计算哈希值,例如:
System.out.println(Objects.hash(null));  //prints: 0
System.out.println(Objects.hash("abc"));  
                                     //prints: 96385
String[] arr = {"abc"};
System.out.println(Objects.hash(arr));
                                     //prints: 96385
Object[] objs = {"a", 42, "c"};
System.out.println(Objects.hash(objs));  
                                    //prints: 124409
System.out.println(Objects.hash("a", 42, "c")); 
                                    //prints: 124409

请注意,hashCode(Object value)方法返回一个不同的哈希值(96354),而Objects.hash(Object... values)方法返回(96385),尽管它们为相同的单个对象计算哈希值。

  1. isNull()nonNull()方法只是布尔表达式的包装器:
  • boolean isNull(Object obj): 返回与obj == null相同的值,例如:
String obj = null;
System.out.println(obj == null);     //prints: true
System.out.println(Objects.isNull(obj));
                                     //prints: true
obj = "";
System.out.println(obj == null);    //prints: false
System.out.println(Objects.isNull(obj));  
                                    //prints: false
    • boolean nonNull(Object obj): 返回与obj != null相同的值,例如:
String obj = null;
System.out.println(obj != null);    //prints: false
System.out.println(Objects.nonNull(obj)); 
                                    //prints: false
obj = "";
System.out.println(obj != null);     //prints: true
System.out.println(Objects.nonNull(obj));  
                                     //prints: true
  1. equals()deepEquals()方法允许我们通过它们的状态来比较两个对象:
  • boolean equals(Object a, Object b): 使用equals(Object)方法比较两个对象,并处理它们中的一个或两个为null的情况,例如:
String o1 = "o";
String o2 = "o";
System.out.println(Objects.equals(o1, o2));       
                                   //prints: true
System.out.println(Objects.equals(null, null));   
                                   //prints: true
Integer[] ints1 = {1,2,3};
Integer[] ints2 = {1,2,3};
System.out.println(Objects.equals(ints1, ints2)); 
                                  //prints: false

在上面的例子中,Objects.equals(ints1, ints2)返回false,因为数组不能覆盖Object类的equals()方法,而是通过引用而不是值进行比较。

    • boolean deepEquals(Object a, Object b): 比较两个数组的元素值,例如:
String o1 = "o";
String o2 = "o";
System.out.println(Objects.deepEquals(o1, o2));    
                                   //prints: true
System.out.println(Objects.deepEquals(null, null));
                                   //prints: true
Integer[] ints1 = {1,2,3};
Integer[] ints2 = {1,2,3};
System.out.println(Objects.deepEquals(ints1,ints2));
                                      //prints: true
Integer[][] iints1 = {{1,2,3},{1,2,3}};
Integer[][] iints2 = {{1,2,3},{1,2,3}};
System.out.println(Objects.
         deepEquals(iints1, iints2)); //prints: true  

正如你所看到的,deepEquals()方法在数组的相应值相等时返回true。但是如果数组有不同的值或相同值的不同顺序,该方法将返回false

Integer[][] iints1 = {{1,2,3},{1,2,3}};
Integer[][] iints2 = {{1,2,3},{1,3,2}};
System.out.println(Objects.
      deepEquals(iints1, iints2)); //prints: false

它是如何工作的...

Arrays.equals(Object a, Object b)Arrays.deepEquals(Object a, Object b)方法的行为与Objects.equals(Object a, Object b)Objects.deepEquals(Object a, Object b)方法相同:

Integer[] ints1 = {1,2,3};
Integer[] ints2 = {1,2,3};
System.out.println(Arrays.equals(ints1, ints2));         
                                            //prints: true
System.out.println(Arrays.deepEquals(ints1, ints2));     
                                            //prints: true
System.out.println(Arrays.equals(iints1, iints2));       
                                            //prints: false
System.out.println(Arrays.deepEquals(iints1, iints2));   
                                            //prints: true

实际上,Arrays.equals(Object a, Object b)Arrays.deepEquals(Object a, Object b)方法在Objects.equals(Object a, Object b)Objects.deepEquals(Object a, Object b)方法的实现中被使用。

总之,如果你想要比较两个对象ab的字段值,那么:

  • 如果它们不是数组且a不是null,则使用a.equals(Object b)

  • 如果它们不是数组且每个或两个对象都可以是null,则使用Objects.equals(Object a, Object b)

  • 如果两者都可以是数组且每个或两者都可以是null,则使用Objects.deepEquals(Object a, Object b)

Objects.deepEquals(Object a, Object b)方法似乎是最安全的,但这并不意味着你必须总是使用它。大多数情况下,你会知道比较的对象是否可以是null或者可以是数组,因此你也可以安全地使用其他方法。