Java-中文官方教程-2022-版-一-

龙哥盟 / 2025-02-21 / 原文

Java 中文官方教程 2022 版(一)

原文:docs.oracle.com/javase/tutorial/reallybigindex.html

教程:入门指南

原文:docs.oracle.com/javase/tutorial/getStarted/index.html

这个教程提供了关于开始使用 Java 编程语言的所有必要信息。

提供了 Java 技术作为一个整体的概述。它讨论了 Java 编程语言和平台,广泛概述了这项技术可以做什么以及如何让您的生活更轻松。

这种实践方法描述了如何下载、安装以及输入什么内容,来创建一个简单的 "Hello World!" 应用程序。它为 NetBeans™ 集成开发环境(NetBeans IDE)、Microsoft Windows、Solaris™ 操作系统(Solaris OS)、Linux 和 Mac 用户提供了单独的说明。

讨论了 "Hello World!" 应用程序,详细描述了代码的每个部分。它涵盖了源代码注释、HelloWorldApp 类定义块以及 main 方法。

如果您在编译或运行本教程中的程序时遇到问题,这就是您应该去的地方。

课程:Java 技术现象

原文:docs.oracle.com/javase/tutorial/getStarted/intro/index.html

谈论 Java 技术似乎无处不在,但它究竟是什么?以下部分解释了 Java 技术既是一种编程语言又是一个平台,并概述了这项技术对您有何作用。

  • 关于 Java 技术

  • Java 技术能做什么?

  • Java 技术将如何改变我的生活?

关于 Java 技术

原文:docs.oracle.com/javase/tutorial/getStarted/intro/definition.html

Java 技术既是一种编程语言,也是一个平台。

Java 编程语言

Java 编程语言是一种高级语言,可以用以下所有流行词来描述:

|

  • 简单

  • 面向对象

  • 分布式

  • 多线程

  • 动态

|

  • 架构中立

  • 可移植

  • 高性能

  • 健壮

  • 安全

|

前述流行词在《Java 语言环境》一书中有详细解释,该书是由詹姆斯·高斯林和亨利·麦吉尔顿撰写的白皮书。

在 Java 编程语言中,所有源代码首先以.java扩展名结尾的纯文本文件中编写。然后,这些源文件由javac编译器编译成.class文件。.class文件不包含本机于处理器的代码;它包含字节码 —— Java 虚拟机的机器语言^(1)(Java VM)。然后,java启动器工具使用 Java 虚拟机的实例运行你的应用程序。

显示 MyProgram.java、编译器、MyProgram.class、Java 虚拟机和计算机上运行的我的程序的图示

软件开发过程概述。

由于 Java 虚拟机在许多不同操作系统上可用,同样的.class文件能够在 Microsoft Windows、Solaris™操作系统(Solaris OS)、Linux 或 Mac OS 上运行。一些虚拟机,如Java SE HotSpot 概览,在运行时执行额外步骤以提高应用程序性能。这包括诸如查找性能瓶颈和重新编译(为本机代码)频繁使用的代码段等任务。

显示源代码、编译器和 Win32、Solaris OS/Linux 和 Mac OS 上的 Java 虚拟机的图示

通过 Java 虚拟机,同一个应用程序能够在多个平台上运行。

Java 平台

平台是程序运行的硬件或软件环境。我们已经提到了一些最流行的平台,如 Microsoft Windows、Linux、Solaris OS 和 Mac OS。大多数平台可以描述为操作系统和底层硬件的组合。Java 平台与大多数其他平台不同,它是一个仅软件的平台,运行在其他基于硬件的平台之上。

Java 平台有两个组成部分:

  • Java 虚拟机

  • Java 应用程序编程接口(API)

你已经了解了 Java 虚拟机;它是 Java 平台的基础,并被移植到各种基于硬件的平台上。

API 是一个庞大的现成软件组件集合,提供许多有用的功能。它被分组为相关类和接口的库;这些库被称为。下一节,Java 技术能做什么?突出了 API 提供的一些功能。

显示 MyProgram.java、API、Java 虚拟机和基于硬件的平台的图示

API 和 Java 虚拟机使程序与底层硬件隔离。

作为一个独立于平台的环境,Java 平台可能比本机代码慢一些。然而,编译器和虚拟机技术的进步正在使性能接近本机代码,而不会威胁可移植性。

术语"Java 虚拟机"和"JVM"指的是 Java 平台的虚拟机。

Java 技术能做什么?

原文:docs.oracle.com/javase/tutorial/getStarted/intro/cando.html

通用高级 Java 编程语言是一个强大的软件平台。每个 Java 平台的完整实现都提供以下功能:

  • 开发工具:开发工具提供了编译、运行、监控、调试和文档化应用程序所需的一切。作为新开发人员,您将主要使用javac编译器、java启动器和javadoc文档工具。

  • 应用程序编程接口(API):API 提供了 Java 编程语言的核心功能。它提供了一系列有用的类,可以直接在您自己的应用程序中使用。它涵盖了从基本对象到网络和安全性,再到 XML 生成和数据库访问等各种内容。核心 API 非常庞大;要了解其包含的内容,请参阅Java 平台标准版 8 文档。

  • 部署技术:JDK 软件提供了标准机制,如 Java Web Start 软件和 Java 插件软件,用于将您的应用程序部署给最终用户。

  • 用户界面工具包:JavaFX、Swing 和 Java 2D 工具包使得创建复杂的图形用户界面(GUIs)成为可能。

  • 集成库:集成库如 Java IDL API、JDBC API、Java 命名和目录接口(JNDI)API、Java RMI 和基于 Internet 互 ORB 协议技术的 Java 远程方法调用(Java RMI-IIOP 技术)使数据库访问和远程对象操作成为可能。

Java 技术将如何改变我的生活?

原文:docs.oracle.com/javase/tutorial/getStarted/intro/changemylife.html

我们不能保证您学习 Java 编程语言后会获得名声、财富,甚至工作。但它很可能会使您的程序更好,并且比其他语言需要更少的努力。我们相信 Java 技术将帮助您实现以下目标:

  • 快速入门:尽管 Java 编程语言是一种强大的面向对象语言,但对于已经熟悉 C 或 C++的程序员来说,学习起来很容易。

  • 编写更少的代码:对程序指标(类计数、方法计数等)的比较表明,用 Java 编程语言编写的程序比用 C++编写的程序小四倍。

  • 编写更好的代码:Java 编程语言鼓励良好的编码实践,自动垃圾回收帮助您避免内存泄漏。其面向对象性质、JavaBeans™组件架构以及广泛、易于扩展的 API 让您可以重用现有、经过测试的代码,并减少引入的错误。

  • 更快地开发程序:Java 编程语言比 C++更简单,因此在使用它编写时,您的开发时间可能快两倍。您的程序也将需要更少的代码行。

  • 避免平台依赖:通过避免使用其他语言编写的库,您可以使您的程序具有可移植性。

  • 一次编写,到处运行:因为用 Java 编程语言编写的应用程序被编译成机器无关的字节码,它们可以在任何 Java 平台上一致运行。

  • 更轻松地分发软件:使用 Java Web Start 软件,用户只需单击鼠标即可启动您的应用程序。启动时自动进行版本检查,确保用户始终使用您软件的最新版本。如果有更新可用,Java Web Start 软件将自动更新他们的安装。

课程: "Hello World!" 应用程序

原文:docs.oracle.com/javase/tutorial/getStarted/cupojava/index.html

下面列出的各节提供了编译和运行简单的 "Hello World!" 应用程序的详细说明。第一节提供了有关开始使用 NetBeans IDE 的信息,这是一个极大简化软件开发过程的集成开发环境。NetBeans IDE 可在下面列出的所有平台上运行。其余各节提供了针对不使用集成开发环境开始的特定平台的说明。如果遇到问题,请务必查阅常见问题部分;它提供了许多新用户遇到的问题的解决方案。

"Hello World!" for the NetBeans IDE 这些说明适用于 NetBeans IDE 的用户。NetBeans IDE 在 Java 平台上运行,这意味着您可以在任何有 JDK 7 可用的操作系统上使用它。这些操作系统包括 Microsoft Windows、Solaris OS、Linux 和 Mac OS X。我们建议尽可能使用 NetBeans IDE 而不是命令行。

"Hello World!" for Microsoft Windows 这些命令行指令适用于 Windows XP Professional、Windows XP Home、Windows Server 2003、Windows 2000 Professional 和 Windows Vista 的用户。

"Hello World!" for Solaris OS, Linux, and Mac OS X 这些命令行指令适用于 Solaris OS、Linux 和 Mac OS X 的用户。常见问题(及其解决方案)如果您在编译或运行应用程序时遇到问题,请参考此页面。

“Hello World!” 适用于 NetBeans IDE

原文:docs.oracle.com/javase/tutorial/getStarted/cupojava/netbeans.html

是时候编写您的第一个应用程序了!这些详细说明适用于 NetBeans IDE 的用户。NetBeans IDE 运行在 Java 平台上,这意味着您可以在任何有 JDK 可用的操作系统上使用它。这些操作系统包括 Microsoft Windows、Solaris OS、Linux 和 Mac OS X。

  • 一个清单

  • 创建您的第一个应用程序

    • 创建一个 IDE 项目

    • 将 JDK 8 添加到平台列表(如果需要)

    • 向生成的源文件添加代码

    • 编译源文件

    • 运行程序

  • 使用 NetBeans IDE 继续教程


一个清单  一个勾选标记

要编写您的第一个程序,您需要:

  1. Java SE 开发工具包(此示例中选择了 JDK 7)

    • 对于 Microsoft Windows、Solaris OS 和 Linux:Java SE 下载索引 页面

    • 对于 Mac OS X:developer.apple.com

  2. NetBeans IDE

    • 对于所有平台:NetBeans IDE 下载索引 页面

创建您的第一个应用程序

您的第一个应用程序HelloWorldApp将简单地显示问候语“Hello World!” 要创建此程序,您将:

  • 创建一个 IDE 项目

    创建 IDE 项目时,您创建了一个环境,用于构建和运行应用程序。使用 IDE 项目可以消除通常与在命令行上开发相关的配置问题。您可以通过在 IDE 中选择单个菜单项来构建或运行应用程序。

  • 向生成的源文件添加代码

    源文件包含用 Java 编程语言编写的代码,您和其他程序员可以理解。作为创建 IDE 项目的一部分,将自动生成一个骨架源文件。然后,您将修改源文件以添加“Hello World!” 信息。

  • 编译源文件为 .class 文件

    IDE 调用 Java 编程语言编译器javac),它接受您的源文件并将其文本翻译为 Java 虚拟机可以理解的指令。该文件中包含的指令称为字节码

  • 运行程序

    IDE 调用 Java 应用程序启动器工具java),该工具使用 Java 虚拟机来运行您的应用程序。

创建一个 IDE 项目

创建一个 IDE 项目:

  1. 运行程序

    • 在 Microsoft Windows 系统上,您可以在“开始”菜单中使用 NetBeans IDE 项目。

    • 在 Solaris OS 和 Linux 系统上,您可以通过导航到 IDE 的 bin 目录并输入 ./netbeans 来执行 IDE 启动脚本。

    • 在 Mac OS X 系统上,单击 NetBeans IDE 应用程序图标。

  2. 在 NetBeans IDE 中,选择文件 | 新建项目...

    NetBeans IDE,文件 | 新建项目 菜单项已选择。

    NetBeans IDE,文件 | 新建项目 菜单项已选择。

  3. 新建项目向导中,展开Java类别,并选择Java 应用程序,如下图所示:

    NetBeans IDE,新项目向导,选择项目页面。

    NetBeans IDE,新项目向导,选择项目页面。

  4. 在向导的名称和位置页面中,执行以下操作(如下图所示):

    • 项目名称字段中,输入Hello World App

    • 创建主类字段中,输入helloworldapp.HelloWorldApp

      NetBeans IDE,新项目向导,名称和位置页面。

    NetBeans IDE,新项目向导,名称和位置页面。

  5. 单击完成。

项目已创建并在 IDE 中打开。您应该看到以下组件:

  • 项目窗口,其中包含项目组件的树视图,包括源文件、代码依赖的库等。

  • 打开 NetBeans IDE。

  • 导航器窗口,您可以使用它快速在所选类中的元素之间导航。

    打开 HelloWorldApp 项目的 NetBeans IDE。

    打开的 NetBeans IDE 与 HelloWorldApp 项目。


将 JDK 8 添加到平台列表(如果需要)

可能需要将 JDK 8 添加到 IDE 的可用平台列表中。要执行此操作,请选择工具 | Java 平台,如下图所示:

从工具菜单中选择 Java 平台管理器

从工具菜单中选择 Java 平台管理器

如果在已安装平台列表中看不到 JDK 8(可能显示为 1.8 或 1.8.0),请单击添加平台,导航到 JDK 8 安装目录,然后单击完成。现在您应该看到这个新添加的平台:

从工具菜单中选择 Java 平台管理器

Java 平台管理器

要将此 JDK 设置为所有项目的默认 JDK,您可以在命令行上使用 --jdkhome 开关运行 IDE,或者在 INSTALLATION_DIRECTORY/etc/netbeans.conf 文件的 netbeans_j2sdkhome 属性中输入 JDK 的路径。

要为当前项目指定此 JDK,请在项目窗格中选择Hello World App,选择文件 | 项目属性(Hello World App),单击,然后在Java 平台下拉菜单中选择JDK 1.8。您应该看到类似以下内容的屏幕:

IDE 现在已配置为 JDK 8。


向生成的源文件添加代码

在创建此项目时,在新建项目向导中保留了创建主类复选框。因此,IDE 已为您创建了一个骨架类。您可以通过替换以下行将“Hello World!” 信息添加到骨架代码中:

// TODO code application logic here

用以下行:

System.out.println("Hello World!"); // Display the string.

可选地,您可以替换这四行生成的代码:

/**
 *
 * @author
 */

用以下行:

/**
 * The HelloWorldApp class implements an application that
 * simply prints "Hello World!" to standard output.
 */

这四行是代码注释,不会影响程序的运行。本教程的后续部分将解释代码注释的用法和格式。

在输入时要小心 大写字母 A   小写字母 a


注意:确保按照所示输入所有代码、命令和文件名。编译器(javac)和启动器(java)都是区分大小写的,因此您必须保持一致。

HelloWorldApphelloworldapp不同


通过选择文件 | 保存来保存您的更改。

文件应该该看起来类似于以下内容:

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package helloworldapp;

/**
 * The HelloWorldApp class implements an application that
 * simply prints "Hello World!" to standard output.
 */
public class HelloWorldApp {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        System.out.println("Hello World!"); // Display the string.
    }

}

编译源文件为 .class 文件

要编译您的源文件,请从 IDE 的主菜单中选择运行 | 构建项目(Hello World App)

输出窗口将打开并显示类似于以下图示的输出:

输出窗口显示 HelloWorld 项目构建的结果。

输出窗口显示 HelloWorld 项目构建的结果。

如果构建输出以“构建成功”结束,则恭喜!您已成功编译了您的程序!

如果构建输出以“构建失败”结束,则您的代码中可能存在语法错误。错误将作为超链接文本显示在输出窗口中。双击此类超链接可导航到错误源。然后,您可以修复错误,再次选择运行 | 构建项目

构建项目时,将生成字节码文件HelloWorldApp.class。您可以通过打开文件窗口并展开Hello World App/build/classes/helloworldapp节点来查看新文件生成的位置,如下图所示。

文件窗口,显示生成的.class 文件。

文件窗口,显示生成的.class文件。

现在您已经构建了项目,可以运行您的程序。

运行程序

从 IDE 的菜单栏中,选择运行 | 运行主项目

下图显示了您现在应该看到的内容。

程序将“Hello World!”打印到输出窗口(以及构建脚本的其他输出)。

程序将“Hello World!”打印到输出窗口(以及构建脚本的其他输出)。

恭喜!您的程序正常运行!

使用 NetBeans IDE 继续教程

教程的接下来几页将解释这个简单应用程序中的代码。之后,课程将深入探讨核心语言特性,并提供更多示例。尽管教程的其余部分没有提供有关使用 NetBeans IDE 的具体说明,但您可以轻松使用 IDE 编写和运行示例代码。以下是一些关于使用 IDE 的提示以及您可能会看到的一些 IDE 行为的解释:

  • 在 IDE 中创建项目后,您可以使用新建文件向导将文件添加到项目中。选择文件 | 新建文件,然后在向导中选择模板,例如空的 Java 文件模板。

  • 您可以使用 IDE 的编译文件(F9)和运行文件(Shift-F6)命令编译和运行单个文件(而不是整个项目)。如果您使用运行主项目命令,IDE 将运行 IDE 关联为主项目的主类的文件。因此,如果您在 HelloWorldApp 项目中创建了一个额外的类,然后尝试使用运行主项目命令运行该文件,IDE 将运行HelloWorldApp文件。

  • 您可能希望为包含多个源文件的示例应用程序创建单独的 IDE 项目。

  • 在 IDE 中输入时,代码完成框可能会定期出现。您可以选择忽略代码完成框并继续输入,或者选择建议的表达式之一。如果您不希望代码完成框自动出现,可以关闭该功能。选择工具 | 选项 | 编辑器,单击代码完成选项卡并清除自动弹出完成窗口复选框。

  • 如果您想要重命名项目窗口中源文件的节点,请从 IDE 的主菜单中选择重构。IDE 会提示您使用重命名对话框引导您完成重命名类和更新引用该类的代码的选项。进行更改并单击重构以应用更改。如果您的项目中只有一个类,这一系列点击可能看起来是多余的,但是在更大的项目中,当您的更改影响代码的其他部分时,这是非常有用的。

  • 要了解 NetBeans IDE 功能的更详尽指南,请参阅NetBeans 文档页面。

“Hello World!” for Microsoft Windows

原文:docs.oracle.com/javase/tutorial/getStarted/cupojava/win32.html

是时候编写你的第一个应用程序了!以下说明适用于 Windows Vista、Windows 7 和 Windows 8 的用户。其他平台的说明在"Hello World!" for Solaris OS, Linux, and Mac OS X 和"Hello World!" for the NetBeans IDE 中。

如果在本页的说明中遇到问题,请参考常见问题(及其解决方案)。

  • 一个清单

  • 创建你的第一个应用程序

    • 创建一个源文件

    • 将源文件编译为.class文件

    • 运行程序


一个清单  一个勾选标记

要编写你的第一个程序,你需要:

  1. Java SE 开发工具包 8(JDK 8)

    你可以立即下载 Windows 版本。(确保你下载的是JDK而不是JRE。)请参考安装说明。

  2. 一个文本编辑器

    在这个例子中,我们将使用 Notepad,它是 Windows 平台上包含的一个简单编辑器。如果你使用不同的文本编辑器,你可以轻松地调整这些说明。

这两个项目是你编写第一个应用程序所需的全部内容。


创建你的第一个应用程序

你的第一个应用程序HelloWorldApp将简单地显示问候语“Hello world!”。为了创建这个程序,你将:

  • 创建一个源文件

    一个源文件包含用 Java 编程语言编写的代码,你和其他程序员都可以理解。你可以使用任何文本编辑器来创建和编辑源文件。

  • 编译源文件为 .class 文件

    Java 编程语言编译器javac)接受你的源文件并将其文本翻译为 Java 虚拟机可以理解的指令。该文件中包含的指令称为字节码

  • 运行程序

    Java 应用程序启动器工具java)使用 Java 虚拟机来运行你的应用程序。

创建一个源文件

要创建一个源文件,你有两个选项:

  • 你可以在计算机上保存文件HelloWorldApp.java,避免大量输入。然后,你可以直接转到将源文件编译为.class文件。

  • 或者,你可以使用以下(更长的)说明。

首先,启动你的编辑器。你可以从开始菜单中选择程序 > 附件 > 记事本来启动记事本编辑器。在一个新文档中,输入以下代码:

/**
 * The HelloWorldApp class implements an application that
 * simply prints "Hello World!" to standard output.
 */
class HelloWorldApp {
    public static void main(String[] args) {
        System.out.println("Hello World!"); // Display the string.
    }
}

输入时要小心 大写字母 A   小写字母 A


注意:请按照所示的方式输入所有代码、命令和文件名。编译器(javac)和启动器(java)都是区分大小写的,因此你必须保持一致。

HelloWorldApphelloworldapp相同。


将代码保存在名为HelloWorldApp.java的文件中。在 Notepad 中执行此操作,首先选择文件 > 另存为...菜单项。然后,在另存为对话框中:

  1. 使用保存位置组合框,指定你将保存文件的文件夹(目录)。在这个例子中,目录是C驱动器上的myapplication

  2. 文件名文本字段中,输入"HelloWorldApp.java",不包括引号。

  3. 另存为类型组合框中选择文本文档(*.txt)

  4. 编码组合框中,将编码保留为 ANSI。

当完成后,对话框应该看起来像这样。

TEXT 另存为对话框,如文本中所述。

在你点击保存之前的另存为对话框。

现在点击保存,然后退出 Notepad。

编译源文件为 .class 文件

打开一个 shell 或“命令”窗口。你可以从开始菜单中选择运行...,然后输入cmd来执行此操作。shell 窗口应该类似于以下图示。

一个窗口,你可以输入 DOS 命令

一个 shell 窗口。

提示符显示你的当前目录。当你打开提示符时,你的当前目录通常是 Windows XP 的主目录(如前面的图所示)。

要编译你的源文件,请将当前目录更改为文件所在的目录。例如,如果你的源目录是C驱动器上的myapplication,请在提示符处输入以下命令并按Enter

cd C:\myapplication

现在提示符应该变成C:\myapplication>


注意:

要切换到另一个驱动器上的目录,你必须输入一个额外的命令:驱动器的名称。例如,要切换到D驱动器上的myapplication目录,你必须输入D:,如下所示:

C:\>D:

D:\>cd myapplication

D:\myapplication>

如果在提示符处输入dir,你应该看到你的源文件,如下所示:

C:\>cd myapplication

C:\myapplication>dir
 Volume in drive C is System
 Volume Serial Number is F2E8-C8CC

 Directory of C:\myapplication

2014-04-24  01:34 PM    <DIR>          .
2014-04-24  01:34 PM    <DIR>          ..
2014-04-24  01:34 PM               267 HelloWorldApp.java
               1 File(s)            267 bytes
               2 Dir(s)  93,297,991,680 bytes free

C:\myapplication>

现在你已经准备好编译了。在提示符处,输入以下命令并按Enter

javac HelloWorldApp.java

编译器已生成一个字节码文件HelloWorldApp.class。在提示符处,输入dir以查看生成的新文件,如下所示:

C:\myapplication>javac HelloWorldApp.java

C:\myapplication>dir
 Volume in drive C is System
 Volume Serial Number is F2E8-C8CC

 Directory of C:\myapplication

2014-04-24  02:07 PM    <DIR>          .
2014-04-24  02:07 PM    <DIR>          ..
2014-04-24  02:07 PM               432 HelloWorldApp.class
2014-04-24  01:34 PM               267 HelloWorldApp.java
               2 File(s)            699 bytes
               2 Dir(s)  93,298,032,640 bytes free

C:\myapplication>

现在你有了一个.class文件,你可以运行你的程序。

如果在这一步的说明中遇到问题,请参考常见问题(及其解决方案)。

运行程序

在同一个目录中,在提示符处输入以下命令:

java -cp . HelloWorldApp

你应该在屏幕上看到以下内容:

C:\myapplication>java -cp . HelloWorldApp
Hello World!

C:\myapplication>

恭喜!你的程序运行成功!

如果在这一步的说明中遇到问题,请参考常见问题(及其解决方案)。

“Hello World!”适用于 Solaris OS、Linux 和 Mac OS X

原文:docs.oracle.com/javase/tutorial/getStarted/cupojava/unix.html

是时候写你的第一个应用程序了!这些详细说明适用于 Solaris OS、Linux 和 Mac OS X 的用户。其他平台的说明在"Hello World!" for Microsoft Windows 和"Hello World!" for the NetBeans IDE 中。

如果你在本页的说明中遇到问题,请查阅常见问题(及其解决方案)。

  • 一个清单

  • 创建你的第一个应用程序

    • 创建一个源文件

    • 将源文件编译为.class文件

    • 运行程序


一个清单  一个勾选标记

要编写你的第一个程序,你需要:

  1. Java SE 开发工具包 8(JDK 8)

    你可以下载适用于 Solaris OS、Linux 或 Mac OS X 的版本。(确保你下载的是JDK而不是JRE。)请查阅安装说明。

  2. 一个文本编辑器

    在这个例子中,我们将使用 Pico,一个适用于许多基于 UNIX 的平台的编辑器。如果你使用不同的文本编辑器,如viemacs,你可以轻松地调整这些说明。

这两个项目是你写第一个应用程序所需的全部。


创建你的第一个应用程序

你的第一个应用程序,HelloWorldApp,将简单地显示问候语“Hello world!”。要创建这个程序,你需要:

  • 创建一个源文件

    一个源文件包含用 Java 编程语言编写的代码,你和其他程序员都可以理解。你可以使用任何文本编辑器来创建和编辑源文件。

  • 将源文件编译为.class 文件

    Java 编程语言的编译器(javac)会获取你的源文件并将其文本转换为 Java 虚拟机可以理解的指令。这个.class文件中包含的指令被称为字节码

  • 运行程序

    Java 应用程序启动器工具(java)使用 Java 虚拟机来运行你的应用程序。

创建一个源文件

要创建一个源文件,你有两个选项:

  • 你可以将文件HelloWorldApp.java保存在你的计算机上,避免大量输入。然后,你可以直接转到编译源文件。

  • 或者,你可以使用以下(更长的)说明。

首先,打开一个 shell 或“终端”窗口。

一个新的终端窗口。

一个新的终端窗口。

当你首次打开提示符时,你的当前目录通常是你的主目录。你可以随时通过在提示符中输入cd然后按Return来将当前目录更改为主目录。

您创建的源文件应保存在单独的目录中。您可以使用命令mkdir创建目录。例如,要在/tmp目录中创建examples/java目录,请使用以下命令:

cd /tmp
mkdir examples
cd examples
mkdir java

要将当前目录更改为此新目录,请输入:

cd /tmp/examples/java

现在您可以开始创建源文件了。

输入pico并按Return键启动 Pico 编辑器。如果系统显示pico: command not found消息,则 Pico 很可能不可用。请咨询系统管理员获取更多信息,或使用其他编辑器。

启动 Pico 时,它将显示一个新的空缓冲区。这是您将输入代码的区域。

将以下代码输入到新缓冲区中:

/**
 * The HelloWorldApp class implements an application that
 * simply prints "Hello World!" to standard output.
 */
class HelloWorldApp {
    public static void main(String[] args) {
        System.out.println("Hello World!"); // Display the string.
    }
}

输入时请小心 大写字母 A   小写字母 a


注意:请按照所示的方式输入所有代码、命令和文件名。编译器(javac)和启动器(java)都区分大小写,因此您必须保持一致。

HelloWorldApphelloworldapp相同。


将代码保存到名为HelloWorldApp.java的文件中。在 Pico 编辑器中,通过输入Ctrl-O,然后在底部看到提示File Name to write:处,输入您希望创建文件的目录,然后输入HelloWorldApp.java。例如,如果您希望在目录/tmp/examples/java中保存HelloWorldApp.java,则输入/tmp/examples/java/HelloWorldApp.java并按Return键。

您可以输入Ctrl-X退出 Pico。

编译源文件为.class文件

打开另一个 shell 窗口。要编译源文件,请将当前目录更改为文件所在的目录。例如,如果您的源目录是/tmp/examples/java,请在提示符处输入以下命令并按Return键:

cd /tmp/examples/java

如果在提示符处输入pwd,您应该看到当前目录,本示例中已更改为/tmp/examples/java

如果在提示符处输入ls,您应该看到您的文件。

命令的结果,显示源文件。

ls命令的结果,显示.java源文件。

现在可以编译源文件了。在提示符处,输入以下命令并按Return键。

javac HelloWorldApp.java

编译器已生成一个字节码文件HelloWorldApp.class。在提示符处,输入ls查看生成的新文件:如下图所示。

命令的结果,显示生成的文件。

ls命令的结果,显示生成的.class文件。

现在您有了一个.class文件,可以运行您的程序了。

如果您在此步骤中遇到问题,请参考常见问题(及其解决方案)。

运行程序

在相同的目录中,在提示符下输入:

java HelloWorldApp

接下来的图示展示了你现在应该看到的内容。

输出将 Hello World!打印到屏幕上。

输出将"Hello World!"打印到屏幕上。

恭喜!你的程序正常运行!

如果在这一步的说明中遇到问题,请参考常见问题(及其解决方案)。

教程:深入了解 "Hello World!" 应用程序

原文:docs.oracle.com/javase/tutorial/getStarted/application/index.html

现在你已经看到了 "Hello World!" 应用程序(甚至可能已经编译并运行了它),你可能想知道它是如何工作的。这里再次是它的代码:


class HelloWorldApp {
    public static void main(String[] args) {
        System.out.println("Hello World!"); // Display the string.
    }
}

"Hello World!" 应用程序由三个主要组件组成:源代码注释、HelloWorldApp 类定义 和 main 方法。以下解释将为您提供对代码的基本理解,但更深层次的含义只有在您完成阅读本教程的其余部分后才会显现。

源代码注释

下面的加粗文本定义了 "Hello World!" 应用程序的注释

/**
 * The HelloWorldApp class implements an application that
 * simply prints "Hello World!" to standard output.
 */
class HelloWorldApp {
    public static void main(String[] args) {
        System.out.println("Hello World!"); // Display the string.
    }
}

注释被编译器忽略,但对其他程序员有用。Java 编程语言支持三种类型的注释:

/* *文本* */

编译器忽略从 /**/ 的所有内容。

/** *文档* */

这表示一个文档注释(doc comment)。编译器忽略这种类型的注释,就像它忽略使用 /**/ 的注释一样。javadoc 工具在准备自动生成的文档时使用文档注释。有关 javadoc 的更多信息,请参阅Javadoc™ 工具文档。

// *文本*

编译器忽略从 // 到行尾的所有内容。

HelloWorldApp 类定义

下面的加粗文本开始了 "Hello World!" 应用程序的类定义块:

/**
 * The HelloWorldApp class implements an application that
 * simply displays "Hello World!" to the standard output.
 */
class HelloWorldApp {
    public static void main(String[] args) {
        System.out.println("Hello World!"); // Display the string.
    }
}

如上所示,类定义的最基本形式是:

class *name* {
    . . .
}

关键字 class 开始了名为 name 的类的类定义,每个类的代码都出现在上面加粗的大括号之间。第二章概述了一般的类,第四章详细讨论了类。现在知道每个应用程序都以类定义开始就足够了。

main 方法

下面的加粗文本开始了 main 方法的定义:

/**
 * The HelloWorldApp class implements an application that
 * simply displays "Hello World!" to the standard output.
 */
class HelloWorldApp {
    public static void main(String[] args) {
        System.out.println("Hello World!"); //Display the string.
    }
}

在 Java 编程语言中,每个应用程序必须包含一个签名为 main 的方法:

public static void main(String[] args)

修饰符 publicstatic 可以以任何顺序编写(public staticstatic public),但约定是使用如上所示的 public static。你可以将参数命名为任何你想要的东西,但大多数程序员选择 "args" 或 "argv"。

main 方法类似于 C 和 C++ 中的 main 函数;它是应用程序的入口点,并随后将调用程序所需的所有其他方法。

main 方法接受一个参数:类型为 String 的元素数组。

public static void main(String[] args)

这个数组是运行时系统向你的应用程序传递信息的机制。例如:

java *MyApp* *arg1* *arg2*

数组中的每个字符串被称为命令行参数。命令行参数让用户在不重新编译应用程序的情况下影响其操作。例如,一个排序程序可能允许用户通过以下命令行参数指定按降序排序数据:

-descending

"Hello World!" 应用程序忽略了它的命令行参数,但你应该意识到这些参数确实存在。

最后,这行代码:

System.out.println("Hello World!");

使用核心库中的System类将"Hello World!"消息打印到标准输出。本教程的其余部分将讨论该库的部分内容(也称为"应用程序编程接口"或"API")。

问题和练习:入门指南

原文:docs.oracle.com/javase/tutorial/getStarted/QandE/questions.html

问题

问题 1:当编译用 Java 编写的程序时,编译器将人类可读的源文件转换为 Java 虚拟机可以理解的平台无关代码。这个平台无关代码叫什么?

问题 2:以下哪个不是有效的注释:

a. /** 注释 */

b. /* 注释 */

c. /* 注释

d. // 注释

问题 3:如果在运行时看到以下错误,你应该首先检查什么?

Exception in thread "main" java.lang.NoClassDefFoundError:
HelloWorldApp.java.

问题 4main方法的正确签名是什么?

问题 5:在声明main方法时,publicstatic中哪个修饰符必须先出现?

问题 6main方法定义了哪些参数?

练习

练习 1:修改HelloWorldApp.java程序,使其显示Hola Mundo!而不是Hello World!

练习 2:你可以在这里找到一个稍作修改的HelloWorldApp版本:HelloWorldApp2.java

程序出现错误。修复错误以使程序成功编译和运行。错误是什么?

检查你的答案。

教训:常见问题(及其解决方法)

原文:docs.oracle.com/javase/tutorial/getStarted/problems/index.html

编译器问题

Microsoft Windows 系统上的常见错误消息

**'javac' is not recognized as an internal or external command, operable program or batch file**

如果你收到这个错误,Windows 找不到编译器(javac)。

下面是告诉 Windows 如何找到javac的一种方法。假设你将 JDK 安装在C:\jdk1.8.0中。在提示符下,你可以输入以下命令并按回车:

C:\jdk1.8.0\bin\javac HelloWorldApp.java

如果选择此选项,每次编译或运行程序时,都需要在C:\jdk1.8.0\bin\之前加上javacjava命令。为了避免这种额外的输入,可以参考 JDK 8 安装说明中的更新 PATH 变量部分。

**Class names, 'HelloWorldApp', are only accepted if annotation processing is explicitly requested**

如果你收到这个错误,那么在编译程序时忘记包含.java后缀。记住,命令是javac HelloWorldApp.java而不是javac HelloWorldApp

UNIX 系统上的常见错误消息

javac: Command not found

如果你收到这个错误,UNIX 找不到编译器javac

下面是告诉 UNIX 如何找到javac的一种方法。假设你将 JDK 安装在/usr/local/jdk1.8.0中。在提示符下,你可以输入以下命令并按回车:

/usr/local/jdk1.8.0/javac HelloWorldApp.java

注意: 如果选择此选项,每次编译或运行程序时,都需要在javacjava命令之前加上/usr/local/jdk1.8.0/。为了避免这种额外的输入,你可以将这些信息添加到你的 PATH 变量中。具体操作步骤将取决于你当前正在运行的 shell。

**Class names, 'HelloWorldApp', are only accepted if annotation processing is explicitly requested**

如果你收到这个错误,那么在编译程序时忘记包含.java后缀。记住,命令是javac HelloWorldApp.java而不是javac HelloWorldApp

语法错误(所有平台)

如果你在程序的某个部分打错了字,编译器可能会发出语法错误。错误信息通常显示错误的类型、检测到错误的行号、该行的代码以及错误在代码中的位置。以下是由于在语句末尾省略分号(;)而引起的错误:

Testing.java:8: error: ';' expected
            count++
                   ^
1 error

如果你看到任何编译器错误,那么你的程序没有成功编译,编译器没有创建.class文件。仔细检查程序,修复你发现的任何错误,然后再试一次。

语义错误

除了验证你的程序在语法上是否正确之外,编译器还会检查其他基本的正确性。例如,每次使用未初始化的变量时,编译器都会警告你:

Testing.java:8: error: variable count might not have been initialized
            count++;
            ^
Testing.java:9: error: variable count might not have been initialized
        System.out.println("Input has " + count + " chars.");
                                          ^
2 errors

再次,你的程序没有成功编译,编译器没有创建.class文件。修复错误并重试。

运行时问题

Microsoft Windows 系统上的错误消息

主线程中的异常"main" java.lang.NoClassDefFoundError: HelloWorldApp

如果你收到这个错误,java找不到你的字节码文件HelloWorldApp.class

java尝试查找你的.class文件的一个地方是你当前的目录。所以如果你的.class文件在C:\java中,你应该将当前目录更改为那个目录。要更改你的目录,请在提示符下输入以下命令并按回车键:

cd c:\java

提示符应该变成C:\java>。如果你在提示符下输入dir,你应该看到你的.java.class文件。现在再次输入java HelloWorldApp

如果你仍然遇到问题,你可能需要更改你的 CLASSPATH 变量。要查看是否需要这样做,请尝试使用以下命令清除类路径。

set CLASSPATH=

现在再次输入java HelloWorldApp。如果程序现在可以运行,那么你需要更改你的 CLASSPATH 变量。要设置这个变量,请参考 JDK 8 安装说明中的更新 PATH 变量部分。CLASSPATH 变量的设置方式相同。

**找不到或加载主类 HelloWorldApp.class**

初学者程序员常犯的一个常见错误是尝试在编译器创建的.class文件上运行java启动器。例如,如果你尝试用java HelloWorldApp.class而不是java HelloWorldApp来运行你的程序,你会得到这个错误。记住,参数是你想要使用的类名不是文件名。

主线程中的异常"main" java.lang.NoSuchMethodError: main

Java 虚拟机要求你执行的类具有一个main方法,用于开始执行你的应用程序。深入了解"Hello World!"应用程序详细讨论了main方法。

UNIX 系统上的错误消息

主线程中的异常"main" java.lang.NoClassDefFoundError: HelloWorldApp

如果你收到这个错误,java找不到你的字节码文件HelloWorldApp.class

java尝试查找你的字节码文件的一个地方是你当前的目录。所以,例如,如果你的字节码文件在/home/jdoe/java中,你应该将当前目录更改为那个目录。要更改你的目录,请在提示符下输入以下命令并按回车键:

cd /home/jdoe/java

如果你在提示符下输入pwd,你应该看到/home/jdoe/java。如果你在提示符下输入ls,你应该看到你的.java.class文件。现在再次输入java HelloWorldApp

如果你仍然遇到问题,你可能需要更改你的 CLASSPATH 环境变量。要查看是否需要这样做,请尝试使用以下命令清除类路径。

unset CLASSPATH

现在再次输入java HelloWorldApp。如果程序现在可以运行,你需要像上面的 PATH 变量一样更改你的 CLASSPATH 变量。

**主线程中的异常"main"java.lang.NoClassDefFoundError: HelloWorldApp/class**

初学者程序员常犯的一个错误是尝试在编译器创建的.class文件上运行java启动器。例如,如果你尝试用java HelloWorldApp.class而不是java HelloWorldApp来运行你的程序,你会得到这个错误。记住,参数是你想要使用的类名,而不是文件名。

主线程中的异常"main"java.lang.NoSuchMethodError: main

Java 虚拟机要求你执行的类具有一个main方法,用于开始执行你的应用程序。深入了解"Hello World!"应用程序详细讨论了main方法。

小程序或 Java Web Start 应用程序被阻止

如果你通过浏览器运行一个应用程序并收到安全警告说应用程序被阻止,请检查以下项目:

  • 验证 JAR 文件清单中的属性是否针对应用程序运行的环境正确设置。权限属性是必需的。在 NetBeans 项目中,你可以通过在 NetBeans IDE 的文件选项卡中展开项目文件夹并双击manifest.mf来打开清单文件。

  • 验证应用程序是否由有效证书签名,并且证书位于签名者 CA 密钥库中。

  • 如果你正在运行一个本地小程序,建立一个用于测试的 Web 服务器。你也可以将你的应用程序添加到例外站点列表中,在 Java 控制面板的安全选项卡中管理。

教程:学习 Java 语言

原文:docs.oracle.com/javase/tutorial/java/index.html

本教程涵盖了使用 Java 编程语言的基础知识。

教授您面向对象编程背后的核心概念:对象、消息、类和继承。本课程最后展示了这些概念如何转化为代码。如果您已经熟悉面向对象编程,可以跳过本课程。

描述了语言的传统特性,包括变量、数组、数据类型、运算符和控制流。

描述了如何编写创建对象的类,以及如何创建和使用对象。

是一种元数据形式,为编译器提供信息。本课程描述了在程序中有效使用注解的位置和方式。

描述了接口——它们是什么,为什么要编写一个,以及如何编写一个。本节还描述了如何从一个类派生另一个类的方式。也就是说,子类如何从超类继承字段和方法。您将了解到所有类都是从Object类派生的,以及如何修改子类从超类继承的方法。

本课程描述了如何使用NumberString对象。本课程还向您展示如何为输出格式化数据。

是 Java 编程语言的一个强大特性。它们提高了代码的类型安全性,使得更多的错误在编译时可检测到。

是 Java 编程语言的一个特性,帮助您组织和结构化类及其彼此之间的关系。

课程:面向对象编程概念

原文:docs.oracle.com/javase/tutorial/java/concepts/index.html

如果您以前从未使用过面向对象的编程语言,您需要在开始编写任何代码之前学习一些基本概念。本课程将向您介绍对象、类、继承、接口和包。每个讨论重点介绍这些概念如何与现实世界相关联,同时向您介绍 Java 编程语言的语法。

什么是对象?

对象是相关状态和行为的软件包。软件对象通常用于模拟日常生活中找到的现实世界对象。本课程解释了如何在对象内表示状态和行为,介绍了数据封装的概念,并解释了以这种方式设计软件的好处。

什么是类?

类是创建对象的蓝图或原型。本节定义了一个模拟现实世界对象状态和行为的类。它有意专注于基础知识,展示了即使一个简单的类也可以清晰地模拟状态和行为。

什么是继承?

继承为组织和构建软件提供了一个强大而自然的机制。本节解释了类如何从它们的超类继承状态和行为,并解释了如何使用 Java 编程语言提供的简单语法从一个类派生另一个类。

什么是接口?

接口是类与外部世界之间的契约。当一个类实现一个接口时,它承诺提供该接口发布的行为。本节定义了一个简单的接口,并解释了任何实现它的类需要做出的必要更改。

什么是包?

包是一个用于以逻辑方式组织类和接口的命名空间。将代码放入包中使得大型软件项目更易于管理。本节解释了为什么这很有用,并向您介绍了 Java 平台提供的应用程序编程接口(API)。

问题和练习:面向对象编程概念

使用本节中提出的问题和练习来测试你对对象、类、继承、接口和包的理解。

什么是对象?

原文:docs.oracle.com/javase/tutorial/java/concepts/object.html

对象是理解面向对象技术的关键。现在看看周围,你会发现许多现实世界的对象:你的狗,你的桌子,你的电视机,你的自行车。

现实世界的对象共享两个特征:它们都有状态行为。狗有状态(名字、颜色、品种、饥饿)和行为(叫、取东西、摇尾巴)。自行车也有状态(当前档位、当前踏板速度、当前速度)和行为(换档、改变踏板速度、刹车)。识别现实世界对象的状态和行为是开始以面向对象编程思考的好方法。

现在花一分钟观察你周围的现实世界对象。对于每个你看到的对象,问自己两个问题:“这个对象可能处于什么状态?”和“这个对象可能执行什么行为?”。确保写下你的观察。当你这样做时,你会注意到现实世界的对象在复杂性上有所不同;你的台灯可能只有两种可能的状态(开和关)和两种可能的行为(打开、关闭),但你的收音机可能有额外的状态(开、关、当前音量、当前频道)和行为(打开、关闭、增加音量、减小音量、搜索、扫描和调谐)。你还可能注意到一些对象反过来也会包含其他对象。这些现实世界的观察都可以转化为面向对象编程的世界。

一个内部填充有项目的圆圈,周围被灰色楔形包围,代表允许访问内部圆圈的方法。

一个软件对象。

软件对象在概念上类似于现实世界的对象:它们也由状态和相关行为组成。对象将其状态存储在字段(某些编程语言中的变量)中,并通过方法(某些编程语言中的函数)公开其行为。方法操作对象的内部状态,并作为对象间通信的主要机制。隐藏内部状态并要求所有交互通过对象的方法执行被称为数据封装——这是面向对象编程的基本原则。

以自行车为例:

一个对象的图片,带有自行车方法和实例变量。

以软件对象建模的自行车。

通过赋予状态(当前速度、当前踏板速度和当前档位)并提供改变该状态的方法,对象保持控制外部世界如何使用它。例如,如果自行车只有 6 个档位,一个改变档位的方法可以拒绝任何小于 1 或大于 6 的值。

将代码捆绑到单独的软件对象中提供了许多好处,包括:

  1. 模块化:一个对象的源代码可以独立编写和维护,与其他对象的源代码无关。一旦创建,一个对象可以在系统内轻松传递。

  2. 信息隐藏:通过仅与对象的方法交互,其内部实现的细节对外部世界保持隐藏。

  3. 代码重用:如果一个对象已经存在(可能是由另一个软件开发人员编写),你可以在你的程序中使用该对象。这使得专家可以实现/测试/调试复杂的、任务特定的对象,然后你可以相信这些对象在你自己的代码中运行。

  4. 可插拔性和调试便利性:如果一个特定对象出现问题,你可以简单地将其从应用程序中移除,并插入一个不同的对象作为替代。这类似于在现实世界中修复机械问题。如果一个螺栓断了,你只替换,而不是整个机器。

什么是类?

原文:docs.oracle.com/javase/tutorial/java/concepts/class.html

在现实世界中,你经常会发现许多同类的个体对象。可能存在成千上万其他同款同型号的自行车。每辆自行车都是根据相同的蓝图构建的,因此包含相同的组件。在面向对象的术语中,我们说你的自行车是已知为自行车的对象类的实例。一个是创建个体对象的蓝图。

以下的Bicycle类是自行车的一种可能实现:


class Bicycle {

    int cadence = 0;
    int speed = 0;
    int gear = 1;

    void changeCadence(int newValue) {
         cadence = newValue;
    }

    void changeGear(int newValue) {
         gear = newValue;
    }

    void speedUp(int increment) {
         speed = speed + increment;   
    }

    void applyBrakes(int decrement) {
         speed = speed - decrement;
    }

    void printStates() {
         System.out.println("cadence:" +
             cadence + " speed:" + 
             speed + " gear:" + gear);
    }
}

Java 编程语言的语法对你来说可能是新的,但这个类的设计是基于之前讨论的自行车对象的。字段cadencespeedgear代表对象的状态,而方法(changeCadencechangeGearspeedUp等)定义了它与外部世界的交互。

你可能已经注意到Bicycle类中没有包含main方法。那是因为它不是一个完整的应用程序;它只是可能在应用程序中使用的自行车的蓝图。创建和使用新的Bicycle对象的责任属于你的应用程序中的某个其他类。

这里有一个BicycleDemo类,它创建了两个独立的Bicycle对象并调用它们的方法:


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

        // Create two different 
        // Bicycle objects
        Bicycle bike1 = new Bicycle();
        Bicycle bike2 = new Bicycle();

        // Invoke methods on 
        // those objects
        bike1.changeCadence(50);
        bike1.speedUp(10);
        bike1.changeGear(2);
        bike1.printStates();

        bike2.changeCadence(50);
        bike2.speedUp(10);
        bike2.changeGear(2);
        bike2.changeCadence(40);
        bike2.speedUp(10);
        bike2.changeGear(3);
        bike2.printStates();
    }
}

这个测试的输出打印了两辆自行车的结束踏板节奏、速度和档位:

cadence:50 speed:10 gear:2
cadence:40 speed:20 gear:3

什么是继承?

原文:docs.oracle.com/javase/tutorial/java/concepts/inheritance.html

不同类型的对象通常彼此之间有一定的共同点。例如,山地自行车、公路自行车和双人自行车都共享自行车的特征(当前速度、当前踏板节奏、当前齿轮)。但每种自行车也定义了使它们不同的附加特征:双人自行车有两个座位和两套把手;公路自行车有下弯把手;一些山地自行车有额外的链环,使它们具有更低的齿轮比。

面向对象编程允许类从其他类中继承常用的状态和行为。在这个例子中,Bicycle现在成为MountainBikeRoadBikeTandemBike超类。在 Java 编程语言中,每个类都允许有一个直接的超类,而每个超类都有无限数量的子类的潜力:

一个类层次结构图。

自行车类的层次结构。

创建子类的语法很简单。在类声明的开头,使用extends关键字,后面跟上要继承的类的名称:

class MountainBike extends Bicycle {

    // new fields and methods defining 
    // a mountain bike would go here

}

这使得MountainBike拥有与Bicycle相同的所有字段和方法,但允许其代码专注于使其独特的特征。这使得子类的代码易于阅读。然而,您必须小心地记录每个超类定义的状态和行为,因为该代码不会出现在每个子类的源文件中。

什么是接口?

原文:docs.oracle.com/javase/tutorial/java/concepts/interface.html

正如你已经学到的,对象通过暴露的方法定义它们与外部世界的交互。方法构成了对象与外部世界的接口;例如,电视机前面的按钮是你和塑料外壳另一侧的电线之间的接口。你按下“电源”按钮来打开和关闭电视。

在最常见的形式中,一个接口是一组相关方法,这些方法没有具体实现。如果将自行车的行为规定为一个接口,可能如下所示:

interface Bicycle {

    //  wheel revolutions per minute
    void changeCadence(int newValue);

    void changeGear(int newValue);

    void speedUp(int increment);

    void applyBrakes(int decrement);
}

要实现这个接口,你的类的名称会改变(例如,变成一个特定品牌的自行车,比如ACMEBicycle),并且在类声明中使用implements关键字:

class ACMEBicycle implements Bicycle {

    int cadence = 0;
    int speed = 0;
    int gear = 1;

   // The compiler will now require that methods
   // changeCadence, changeGear, speedUp, and applyBrakes
   // all be implemented. Compilation will fail if those
   // methods are missing from this class.

    void changeCadence(int newValue) {
         cadence = newValue;
    }

    void changeGear(int newValue) {
         gear = newValue;
    }

    void speedUp(int increment) {
         speed = speed + increment;   
    }

    void applyBrakes(int decrement) {
         speed = speed - decrement;
    }

    void printStates() {
         System.out.println("cadence:" +
             cadence + " speed:" + 
             speed + " gear:" + gear);
    }
}

实现一个接口允许一个类更加正式地承诺提供的行为。接口形成了类与外部世界之间的契约,这个契约在编译时由编译器强制执行。如果你的类声称实现了一个接口,那么该接口定义的所有方法必须在类的源代码中出现,类才能成功编译。


注意: 要实际编译ACMEBicycle类,你需要在实现的接口方法开头添加public关键字。你将在后面关于类和对象以及接口和继承的课程中了解这样做的原因。


什么是包?

原文:docs.oracle.com/javase/tutorial/java/concepts/package.html

包是一个命名空间,用于组织一组相关的类和接口。从概念上讲,你可以将包想象成计算机上的不同文件夹。你可以将 HTML 页面放在一个文件夹中,将图片放在另一个文件夹中,将脚本或应用程序放在另一个文件夹中。由于用 Java 编写的软件可能由数百甚至数千个单独的类组成,因此将相关的类和接口放入包中进行组织是有意义的。

Java 平台提供了一个庞大的类库(一组包),适用于在你自己的应用程序中使用。这个库被称为“应用程序编程接口”,简称为“API”。它的包代表了与通用编程最常相关的任务。例如,一个String对象包含了字符字符串的状态和行为;一个File对象允许程序员轻松地在文件系统上创建、删除、检查、比较或修改文件;一个Socket对象允许创建和使用网络套接字;各种 GUI 对象控制按钮、复选框以及与图形用户界面相关的任何其他内容。有成千上万的类可供选择。这使得你作为程序员可以专注于设计你特定应用程序的部分,而不是需要使其工作所需的基础设施。

Java 平台 API 规范包含了 Java SE 平台提供的所有包、接口、类、字段和方法的完整列表。在浏览器中加载该页面并将其加为书签。作为程序员,它将成为你最重要的参考文档。

问题和练习:面向对象编程概念

原文:docs.oracle.com/javase/tutorial/java/concepts/QandE/questions.html

问题

  1. 现实世界的对象包含 ___ 和 ___。

  2. 一个软件对象的状态存储在 ___ 中。

  3. 一个软件对象的行为通过 ___ 暴露。

  4. 将内部数据隐藏在外部世界之外,并仅通过公开的方法访问它被称为数据 ___。

  5. 软件对象的蓝图被称为 ___。

  6. 共同行为可以在 ___ 中定义,并通过 ___ 关键字继承到 ___ 中。

  7. 一个没有实现的方法集合被称为 ___。

  8. 通过功能组织类和接口的命名空间被称为 ___。

  9. API 一词代表 ___?

练习

  1. 为你在本教程开始时观察到的每个现实世界对象创建新的类。如果忘记所需的语法,请参考 Bicycle 类。

  2. 对于你上面创建的每个新类,创建一个定义其行为的接口,然后要求你的类实现它。省略一两个方法并尝试编译。错误是什么样的?

检查你的答案。

课程:语言基础

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/index.html

变量

对象将它们的状态存储在字段中,这一点你已经学过了。然而,Java 编程语言也使用术语“变量”。本节讨论了这种关系,以及变量命名规则和约定,基本数据类型(原始类型、字符串和数组),默认值和字面值。

运算符

本节描述了 Java 编程语言的运算符。它首先介绍了最常用的运算符,然后介绍了较少使用的运算符。每个讨论都包括您可以编译和运行的代码示例。

表达式、语句和块

运算符可用于构建计算值的表达式;表达式是语句的核心组件;语句可以分组成块。本节使用您已经看过的示例代码讨论了表达式、语句和块。

控制流语句

本节描述了 Java 编程语言支持的控制流语句。它涵盖了决策、循环和分支语句,使您的程序能够有条件地执行特定的代码块。

变量

译文:docs.oracle.com/javase/tutorial/java/nutsandbolts/variables.html

正如您在上一课中学到的,对象将其状态存储在 字段 中。

int cadence = 0;
int speed = 0;
int gear = 1;

什么是对象? 讨论向您介绍了字段,但您可能仍有一些疑问,比如:字段的命名规则和约定是什么?除了 int,还有哪些其他数据类型?字段在声明时必须初始化吗?如果未明确初始化,字段是否被分配默认值?我们将在本课程中探讨这些问题的答案,但在此之前,您必须首先了解一些技术上的区别。在 Java 编程语言中,术语 "字段" 和 "变量" 都被使用;这是新开发人员常见的困惑源,因为两者似乎经常指的是同一件事情。

Java 编程语言定义了以下类型的变量:

  • 实例变量(非静态字段) 从技术上讲,对象将其各自的状态存储在 "非静态字段" 中,即没有使用 static 关键字声明的字段。非静态字段也被称为 实例变量,因为它们的值对类的每个 实例(换句话说,对每个对象)都是唯一的;一个自行车的 currentSpeed 与另一个自行车的 currentSpeed 是独立的。

  • 类变量(静态字段) 类变量 是使用 static 修饰符声明的任何字段;这告诉编译器,无论类被实例化多少次,该变量都只存在一份副本。为特定类型的自行车定义齿轮数量的字段可以标记为 static,因为概念上相同数量的齿轮将适用于所有实例。代码 static int numGears = 6; 将创建这样一个静态字段。此外,关键字 final 可以添加以指示齿轮数量永远不会改变。

  • 局部变量 类似于对象将其状态存储在字段中,方法通常将其临时状态存储在 局部变量 中。声明局部变量的语法类似于声明字段(例如,int count = 0;)。没有特殊的关键字将变量标记为局部变量;这一决定完全取决于变量声明的位置——即在方法的大括号之间。因此,局部变量仅对声明它们的方法可见;它们无法从类的其他部分访问。

  • 参数 你已经在Bicycle类和"Hello World!"应用程序的main方法中看到了参数的示例。回想一下,main方法的签名是public static void main(String[] args)。在这里,args变量是该方法的参数。重要的是要记住参数总是被分类为"变量"而不是"字段"。这也适用于其他接受参数的结构(如构造函数和异常处理程序),你将在本教程后面学习到。

说到这一点,本教程的其余部分在讨论字段和变量时使用以下一般准则。如果我们在谈论"一般字段"(不包括局部变量和参数),我们可能会简单地说"字段"。如果讨论适用于"以上所有内容",我们可能会简单地说"变量"。如果上下文需要区分,我们将使用适当的具体术语(静态字段、局部变量等)。您还可能偶尔看到术语"成员"被使用。类型的字段、方法和嵌套类型统称为成员

命名

每种编程语言都有其自己的规则和约定,用于允许使用的名称类型,而 Java 编程语言也不例外。关于为变量命名的规则和约定可以总结如下:

  • 变量名区分大小写。变量的名称可以是任何合法标识符——以字母、美元符号"$"或下划线字符"_"开头的 Unicode 字母和数字的无限长度序列。然而,约定是始终以字母开头,而不是"$"或"_"。此外,按照约定,美元符号字符根本不使用。您可能会发现一些情况下自动生成的名称会包含美元符号,但您的变量名应始终避免使用它。下划线字符也存在类似的约定;虽然以"_"开头的变量名在技术上是合法的,但这种做法是不鼓励的。不允许使用空格。

  • 后续字符可以是字母、数字、美元符号或下划线字符。这条规则也适用于约定(和常识)。在为变量选择名称时,使用完整的单词而不是神秘的缩写。这样做将使您的代码更易于阅读和理解。在许多情况下,这也将使您的代码自我记录;例如,命名为cadencespeedgear的字段比缩写版本(如scg)更直观。还要记住,您选择的名称不能是关键字或保留字。

  • 如果您选择的名称只包含一个单词,请将该单词拼写为全部小写字母。如果名称由多个单词组成,请将每个后续单词的第一个字母大写。名称gearRatiocurrentGear是这种约定的典型例子。如果您的变量存储一个常量值,比如static final int NUM_GEARS = 6,约定会略有变化,每个字母都大写,并用下划线字符分隔后续单词。按照约定,下划线字符在其他地方永远不会被使用。

原始数据类型

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

Java 编程语言是静态类型的,这意味着所有变量在使用之前必须先声明。这涉及声明变量的类型和名称,就像你已经看到的那样:

int gear = 1;

这样做告诉您的程序,存在一个名为“gear”的字段,保存数值数据,并具有“1”的初始值。变量的数据类型确定它可能包含的值,以及可以对其执行的操作。除了int之外,Java 编程语言还支持其他七种原始数据类型。原始类型由语言预定义,并由保留关键字命名。原始值不与其他原始值共享状态。Java 编程语言支持的八种原始数据类型是:

  • byte: byte 数据类型是一个 8 位有符号的二进制补码整数。它的最小值为-128,最大值为 127(包括边界值)。在需要实际节省内存的大数组中,byte数据类型可能很有用。它们也可以用于代替int,在这种情况下,它们的限制有助于澄清代码;变量的范围受限可以作为一种文档形式。

  • short: short 数据类型是一个 16 位有符号的二进制补码整数。它的最小值为-32,768,最大值为 32,767(包括边界值)。与byte一样,相同的准则适用:在需要实际节省内存的大数组中,可以使用short来节省内存。

  • int: 默认情况下,int数据类型是一个 32 位有符号的二进制补码整数,其最小值为-2³¹,最大值为 2³¹-1。在 Java SE 8 及更高版本中,可以使用int数据类型来表示无符号的 32 位整数,其最小值为 0,最大值为 2³²-1。使用 Integer 类将int数据类型用作无符号整数。有关更多信息,请参阅“数字类”部分。已向Integer类添加了compareUnsigneddivideUnsigned等静态方法,以支持无符号整数的算术运算。

  • long: long 数据类型是一个 64 位的二进制补码整数。有符号的long的最小值为-2⁶³,最大值为 2⁶³-1。在 Java SE 8 及更高版本中,可以使用long数据类型来表示无符号的 64 位长整数,其最小值为 0,最大值为 2⁶⁴-1。当需要比int提供的值范围更广时,请使用此数据类型。Long类还包含诸如compareUnsigneddivideUnsigned等方法,以支持无符号长整数的算术运算。

  • floatfloat数据类型是一个单精度 32 位 IEEE 754 浮点数。其值的范围超出了本讨论的范围,但在 Java 语言规范的浮点类型、格式和值部分中有规定。与对byteshort的建议一样,如果需要在大量浮点数数组中节省内存,则使用float(而不是double)。这种数据类型永远不应该用于精确值,比如货币。对于这种情况,你将需要使用java.math.BigDecimal类。数字和字符串介绍了BigDecimal和 Java 平台提供的其他有用类。

  • doubledouble数据类型是一个双精度 64 位 IEEE 754 浮点数。其值的范围超出了本讨论的范围,但在 Java 语言规范的浮点类型、格式和值部分中有规定。对于十进制值,这种数据类型通常是默认选择。如上所述,这种数据类型永远不应该用于精确值,比如货币。

  • booleanboolean数据类型只有两个可能的值:truefalse。使用这种数据类型来跟踪真/假条件的简单标志。这种数据类型表示一位信息,但其“大小”并没有明确定义。

  • charchar数据类型是一个单个的 16 位 Unicode 字符。它的最小值为'\u0000'(或 0),最大值为'\uffff'(或 65,535,包括在内)。

除了上面列出的八种基本数据类型外,Java 编程语言还通过java.lang.String类提供了对字符字符串的特殊支持。将字符字符串放在双引号中将自动创建一个新的String对象;例如,String s = "this is a string";String对象是不可变的,这意味着一旦创建,它们的值就不能被更改。String类在技术上不是一个基本数据类型,但考虑到语言对它的特殊支持,你可能会倾向于将其视为基本数据类型。你将在简单数据对象中了解更多关于String类的信息。

默认值

在声明字段时,不总是需要为其赋值。声明但未初始化的字段将由编译器设置为一个合理的默认值。一般来说,这个默认值将是零或null,取决于数据类型。然而,依赖这些默认值通常被认为是不良的编程风格。

以下图表总结了上述数据类型的默认值。

数据类型 默认值(对于字段)
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0d
char '\u0000'
String(或任何对象) null
boolean false

局部变量略有不同;编译器永远不会为未初始化的局部变量分配默认值。如果无法在声明变量的地方初始化局部变量,请确保在尝试使用之前为其赋值。访问未初始化的局部变量将导致编译时错误。

字面值

您可能已经注意到,在初始化原始类型变量时不使用 new 关键字。原始类型是内置到语言中的特殊数据类型;它们不是从类创建的对象。字面值 是固定值的源代码表示形式;字面值直接在您的代码中表示,无需计算。如下所示,可以将字面值分配给原始类型变量:

boolean result = true;
char capitalC = 'C';
byte b = 100;
short s = 10000;
int i = 100000;

整数字面值

如果整数字面值以字母 Ll 结尾,则其类型为 long;否则为 int。建议使用大写字母 L,因为小写字母 l 很难与数字 1 区分。

整型 byteshortintlong 的值可以从 int 字面值创建。超出 int 范围的 long 类型值可以从 long 字面值创建。整数字面值可以用以下数字系统表示:

  • 十进制:基数为 10,其数字由数字 0 到 9 组成;这是您每天使用的数字系统

  • 十六进制:基数为 16,其数字由数字 0 到 9 和字母 A 到 F 组成

  • 二进制:基数为 2,其数字由数字 0 和 1 组成(您可以在 Java SE 7 及更高版本中创建二进制字面值)

对于通用编程,十进制系统可能是您唯一使用的数字系统。但是,如果需要使用其他数字系统,以下示例显示了正确的语法。前缀 0x 表示十六进制,0b 表示二进制:

// The number 26, in decimal
int decVal = 26;
//  The number 26, in hexadecimal
int hexVal = 0x1a;
// The number 26, in binary
int binVal = 0b11010;

浮点字面值

浮点字面值如果以字母 Ff 结尾,则其类型为 float;否则其类型为 double,并且可以选择以字母 Dd 结尾。

浮点类型(floatdouble)也可以使用 E 或 e(科学计数法)、F 或 f(32 位浮点字面值)和 D 或 d(64 位双精度字面值;这是默认值,按照惯例省略)来表示。

double d1 = 123.4;
// same value as d1, but in scientific notation
double d2 = 1.234e2;
float f1  = 123.4f;

字符和字符串字面值

charString类型的字面量可以包含任何 Unicode(UTF-16)字符。如果您的编辑器和文件系统允许,您可以直接在代码中使用这些字符。如果不允许,您可以使用"Unicode 转义",例如'\u0108'(带抑扬符的大写 C),或"S\u00ED Se\u00F1or"(西班牙语中的 Sí Señor)。始终使用'单引号'表示char字面量,使用"双引号"表示String字面量。Unicode 转义序列可以在程序的其他地方使用(例如,在字段名称中),不仅仅在charString字面量中。

Java 编程语言还支持一些特殊的转义序列,用于charString字面量:\b(退格)、\t(制表符)、\n(换行符)、\f(换页符)、\r(回车符)、\"(双引号)、\'(单引号)和\\(反斜杠)。

还有一个特殊的null字面量,可以用作任何引用类型的值。null可以赋给任何变量,除了原始类型的变量。对于null值,除了测试其是否存在外,几乎无法做任何操作。因此,在程序中,null通常用作指示某个对象不可用的标记。

最后,还有一种特殊的字面量称为类字面量,通过取类型名称并附加".class"形成;例如,String.class。这指的是代表类型本身的对象(类型为Class)。

在数字字面量中使用下划线字符

在 Java SE 7 及更高版本中,任何数量的下划线字符(_)可以出现在数字字面量中的任何位置。此功能使您能够,例如,将数字字面量中的数字分组,从而提高代码的可读性。

例如,如果您的代码包含许多位数的数字,您可以使用下划线字符将数字分组为三位一组,类似于您如何使用逗号或空格等标点符号作为分隔符。

以下示例展示了您可以在数字字面量中使用下划线的其他方式:

long creditCardNumber = 1234_5678_9012_3456L;
long socialSecurityNumber = 999_99_9999L;
float pi =  3.14_15F;
long hexBytes = 0xFF_EC_DE_5E;
long hexWords = 0xCAFE_BABE;
long maxLong = 0x7fff_ffff_ffff_ffffL;
byte nybbles = 0b0010_0101;
long bytes = 0b11010010_01101001_10010100_10010010;

您只能在数字之间放置下划线;不能在以下位置放置下划线:

  • 在数字的开头或结尾

  • 邻近浮点字面量中的小数点

  • FL后缀之前

  • 在期望出现一串数字的位置

以下示例演示了数字字面量中有效和无效的下划线放置方式(已突出显示):

// Invalid: cannot put underscores
// adjacent to a decimal point
float pi1 = 3_.1415F;
// Invalid: cannot put underscores 
// adjacent to a decimal point
float pi2 = 3._1415F;
// Invalid: cannot put underscores 
// prior to an L suffix
long socialSecurityNumber1 = 999_99_9999_L;

// OK (decimal literal)
int x1 = 5_2;
// Invalid: cannot put underscores
// At the end of a literal
int x2 = 52_;
// OK (decimal literal)
int x3 = 5_______2;

// Invalid: cannot put underscores
// in the 0x radix prefix
int x4 = 0_x52;
// Invalid: cannot put underscores
// at the beginning of a number
int x5 = 0x_52;
// OK (hexadecimal literal)
int x6 = 0x5_2; 
// Invalid: cannot put underscores
// at the end of a number
int x7 = 0x52_;

数组

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/arrays.html

数组是一个容器对象,它保存单一类型的固定数量的值。数组的长度在创建数组时确定。创建后,其长度是固定的。你已经在"Hello World!"应用程序的main方法中看到了数组的一个示例。本节将更详细地讨论数组。

数组插图,包含 10 个从 0 到 9 编号的方框;索引为 0 表示数组中的第一个元素

一个包含 10 个元素的数组。

数组中的每个项称为元素,每个元素通过其数字索引访问。如前面的插图所示,编号从 0 开始。例如,第 9 个元素将在索引 8 处访问。

下面的程序,ArrayDemo,创建了一个整数数组,向数组中放入一些值,并将每个值打印到标准输出。


class ArrayDemo {
    public static void main(String[] args) {
        // declares an array of integers
        int[] anArray;

        // allocates memory for 10 integers
        anArray = new int[10];

        // initialize first element
        anArray[0] = 100;
        // initialize second element
        anArray[1] = 200;
        // and so forth
        anArray[2] = 300;
        anArray[3] = 400;
        anArray[4] = 500;
        anArray[5] = 600;
        anArray[6] = 700;
        anArray[7] = 800;
        anArray[8] = 900;
        anArray[9] = 1000;

        System.out.println("Element at index 0: "
                           + anArray[0]);
        System.out.println("Element at index 1: "
                           + anArray[1]);
        System.out.println("Element at index 2: "
                           + anArray[2]);
        System.out.println("Element at index 3: "
                           + anArray[3]);
        System.out.println("Element at index 4: "
                           + anArray[4]);
        System.out.println("Element at index 5: "
                           + anArray[5]);
        System.out.println("Element at index 6: "
                           + anArray[6]);
        System.out.println("Element at index 7: "
                           + anArray[7]);
        System.out.println("Element at index 8: "
                           + anArray[8]);
        System.out.println("Element at index 9: "
                           + anArray[9]);
    }
} 

该程序的输出是:

Element at index 0: 100
Element at index 1: 200
Element at index 2: 300
Element at index 3: 400
Element at index 4: 500
Element at index 5: 600
Element at index 6: 700
Element at index 7: 800
Element at index 8: 900
Element at index 9: 1000

在实际的编程情况中,你可能会使用支持的循环结构之一来遍历数组的每个元素,而不是像前面的示例中那样逐行编写。然而,该示例清楚地说明了数组的语法。你将在 Control Flow 部分学习各种循环结构(forwhile和`do-while)。

声明一个变量来引用一个数组

前面的程序用以下代码声明了一个数组(名为anArray):

// declares an array of integers
int[] anArray;

与其他类型的变量声明类似,数组声明有两个组成部分:数组的类型和数组的名称。数组的类型写作*type*[],其中*type*是包含元素的数据类型;方括号是特殊符号,表示该变量保存一个数组。数组的大小不是其类型的一部分(这就是为什么方括号是空的)。数组的名称可以是任何你想要的,只要遵循之前在 naming 部分讨论的规则和约定。与其他类型的变量一样,声明并不会实际创建一个数组;它只是告诉编译器这个变量将保存指定类型的数组。

同样,你也可以声明其他类型的数组:

byte[] anArrayOfBytes;
short[] anArrayOfShorts;
long[] anArrayOfLongs;
float[] anArrayOfFloats;
double[] anArrayOfDoubles;
boolean[] anArrayOfBooleans;
char[] anArrayOfChars;
String[] anArrayOfStrings;

你也可以将方括号放在数组名称后面:

// this form is discouraged
float anArrayOfFloats[];

然而,约定不鼓励这种形式;方括号标识数组类型,应该与类型标识一起出现。

创建、初始化和访问数组

new运算符创建数组的一种方法。ArrayDemo程序中的下一条语句分配了足够内存以容纳 10 个整数元素的数组,并将该数组分配给anArray变量。

// create an array of integers
anArray = new int[10];

如果缺少这个语句,那么编译器会打印如下错误,并且编译失败:

ArrayDemo.java:4: Variable anArray may not have been initialized.

接下来的几行为数组的每个元素赋值:

anArray[0] = 100; // initialize first element
anArray[1] = 200; // initialize second element
anArray[2] = 300; // and so forth

每个数组元素都通过其数字索引访问:

System.out.println("Element 1 at index 0: " + anArray[0]);
System.out.println("Element 2 at index 1: " + anArray[1]);
System.out.println("Element 3 at index 2: " + anArray[2]);

或者,您可以使用快捷语法来创建和初始化一个数组:

int[] anArray = { 
    100, 200, 300,
    400, 500, 600, 
    700, 800, 900, 1000
};

这里数组的长度由大括号中提供的数值数量和逗号分隔确定。

您还可以通过使用两个或更多组括号(如String[][] names)声明一个数组的数组(也称为多维数组)。因此,必须通过相应数量的索引值访问每个元素。

在 Java 编程语言中,多维数组是其组件本身为数组的数组。这与 C 或 Fortran 中的数组不同。由此产生的一个结果是,行的长度允许变化,如下面的MultiDimArrayDemo程序所示:

class MultiDimArrayDemo {
    public static void main(String[] args) {
        String[][] names = {
            {"Mr. ", "Mrs. ", "Ms. "},
            {"Smith", "Jones"}
        };
        // Mr. Smith
        System.out.println(names[0][0] + names[1][0]);
        // Ms. Jones
        System.out.println(names[0][2] + names[1][1]);
    }
}

此程序的输出为:

Mr. Smith
Ms. Jones

最后,您可以使用内置的length属性来确定任何数组的大小。以下代码将数组的大小打印到标准输出:

 System.out.println(anArray.length);

复制数组

System类有一个arraycopy方法,您可以使用它来高效地将数据从一个数组复制到另一个数组中:

public static void arraycopy(Object src, int srcPos,
                             Object dest, int destPos, int length)

两个Object参数指定要从中复制的数组和要复制到的数组目标。三个int参数指定源数组中的起始位置,目标数组中的起始位置,以及要复制的数组元素数量。

以下程序,ArrayCopyDemo,声明了一个String元素数组。它使用System.arraycopy方法将数组组件的子序列复制到第二个数组中:


class ArrayCopyDemo {
    public static void main(String[] args) {
        String[] copyFrom = {
            "Affogato", "Americano", "Cappuccino", "Corretto", "Cortado",   
            "Doppio", "Espresso", "Frappucino", "Freddo", "Lungo", "Macchiato",      
            "Marocchino", "Ristretto" };

        String[] copyTo = new String[7];
        System.arraycopy(copyFrom, 2, copyTo, 0, 7);
        for (String coffee : copyTo) {
            System.out.print(coffee + " ");           
        }
    }
}

此程序的输出为:

Cappuccino Corretto Cortado Doppio Espresso Frappucino Freddo 

数组操作

数组是编程中使用的一个强大而有用的概念。Java SE 提供了一些用于执行与数组相关的最常见操作的方法。例如,ArrayCopyDemo示例使用System类的arraycopy方法,而不是手动迭代源数组的元素并将每个元素放入目标数组中。这是在后台执行的,使开发人员只需使用一行代码调用该方法。

为了方便起见,Java SE 在java.util.Arrays类中提供了几种执行数组操作(如复制、排序和搜索数组等常见任务)的方法。例如,前面的示例可以修改为使用java.util.Arrays类的copyOfRange方法,如您可以在ArrayCopyOfDemo示例中看到的那样。不同之处在于,使用copyOfRange方法不需要在调用方法之前创建目标数组,因为目标数组是由该方法返回的:


class ArrayCopyOfDemo {
    public static void main(String[] args) {
        String[] copyFrom = {
            "Affogato", "Americano", "Cappuccino", "Corretto", "Cortado",   
            "Doppio", "Espresso", "Frappucino", "Freddo", "Lungo", "Macchiato",      
            "Marocchino", "Ristretto" };

        String[] copyTo = java.util.Arrays.copyOfRange(copyFrom, 2, 9);        
        for (String coffee : copyTo) {
            System.out.print(coffee + " ");           
        }            
    }
}

如您所见,该程序的输出相同,尽管代码行数更少。请注意,copyOfRange 方法的第二个参数是要复制的范围的初始索引(包括),而第三个参数是要复制的范围的最终索引,不包括。在此示例中,要复制的范围不包括索引为 9 的数组元素(其中包含字符串Lungo)。

java.util.Arrays 类中的一些其他有用操作包括:

  • 搜索数组以找到特定值的索引位置(binarySearch 方法)。

  • 比较两个数组以确定它们是否相等(equals 方法)。

  • 填充数组以在每个索引处放置特定值(fill 方法)。

  • 将数组按升序排序。可以顺序执行,使用sort 方法,也可以并行执行,使用 Java SE 8 中引入的parallelSort 方法。在多处理器系统上,并行排序大型数组比顺序数组排序更快。

  • 创建使用数组作为其源的流(stream 方法)。例如,以下语句以与前面示例相同的方式打印copyTo数组的内容:

    java.util.Arrays.stream(copyTo).map(coffee -> coffee + " ").forEach(System.out::print);  
    
    

    有关流的更多信息,请参见聚合操作。

  • 将数组转换为字符串。toString 方法将数组的每个元素转换为字符串,用逗号分隔,然后用括号括起来。例如,以下语句将copyTo数组转换为字符串并打印出来:

    System.out.println(java.util.Arrays.toString(copyTo)); 
    
    

    此语句打印如下内容:

    [Cappuccino, Corretto, Cortado, Doppio, Espresso, Frappucino, Freddo] 
    
    

变量总结

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/variablesummary.html

Java 编程语言在其术语中同时使用“字段”和“变量”。实例变量(非静态字段)对于类的每个实例都是唯一的。类变量(静态字段)是用static修饰的字段;无论类被实例化多少次,类变量都只有一个副本。局部变量在方法内部存储临时状态。参数是向方法提供额外信息的变量;局部变量和参数始终被分类为“变量”(而不是“字段”)。在命名字段或变量时,有一些规则和约定您应该(或必须)遵循。

八种基本数据类型分别为:byte、short、int、long、float、double、boolean 和 char。java.lang.String 类表示字符串。编译器会为上述类型的字段分配一个合理的默认值;对于局部变量,永远不会分配默认值。字面值是固定值的源代码表示。数组是一个容器对象,它保存了单一类型的固定数量的值。数组的长度在创建数组时确定,创建后其长度是固定的。

问题和练习:变量

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/QandE/questions_variables.html

问题

  1. "实例变量"是 ___ 的另一个名称。

  2. "类变量"是 ___ 的另一个名称。

  3. 本地变量存储临时状态;它声明在 ___ 内。

  4. 在方法签名的括号内声明的变量称为 ____。

  5. Java 编程语言支持的八种基本数据类型是什么?

  6. 字符串由类 ___ 表示。

  7. 一个 ___ 是一个容器对象,它保存单一类型的固定数量的值。

练习

  1. 创建一个定义一些字段的小程序。尝试创建一些非法的字段名称,看看编译器会产生什么样的错误。使用命名规则和约定作为指南。

  2. 在你在练习 1 中创建的程序中,尝试将字段保持未初始化并打印出它们的值。尝试对局部变量执行相同操作,看看你能产生什么样的编译器错误。熟悉常见的编译器错误将使您更容易识别代码中的错误。

检查你的答案

运算符

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/operators.html

现在你已经学会了如何声明和初始化变量,你可能想知道如何对其进行操作。学习 Java 编程语言的运算符是一个很好的开始。运算符是执行特定操作的特殊符号,作用于一个、两个或三个操作数,然后返回一个结果。

当我们探索 Java 编程语言的运算符时,提前了解哪些运算符具有最高优先级可能会对您有所帮助。下表中的运算符按照优先级顺序列出。出现在表格顶部的运算符优先级较高。优先级较高的运算符在相对较低优先级的运算符之前进行评估。同一行上的运算符具有相同的优先级。当具有相同优先级的运算符出现在同一表达式中时,必须有规则来决定哪个首先进行评估。除了赋值运算符之外的所有二元运算符都是从左到右进行评估;赋值运算符是从右到左进行评估。

运算符优先级

运算符 优先级
后缀 *expr*++ *expr*--
一元 ++*expr* --*expr* +*expr* -*expr* ~ !
乘法 * / %
加法 + -
移位 << >> >>>
关系 < > <= >= instanceof
相等 == !=
按位与 &
按位异或 ^
按位或 &#124;
逻辑与 &&
逻辑或 &#124;&#124;
三元 ? :
赋值 = += -= *= /= %= &= ^= &#124;= <<= >>= >>>=

在通用编程中,某些运算符比其他运算符更频繁地出现;例如,赋值运算符"="比无符号右移运算符">>>"更常见。因此,以下讨论首先关注您最有可能经常使用的运算符,最后关注那些不太常见的运算符。每个讨论都附有您可以编译和运行的示例代码。研究其输出将有助于巩固您刚刚学到的知识。

赋值、算术和一元运算符

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/op1.html

简单赋值运算符

您将遇到的最常见运算符之一是简单赋值运算符"="。您在自行车类中看到了这个运算符;它将右侧的值分配给左侧的操作数:

 int cadence = 0;
 int speed = 0;
 int gear = 1;

这个运算符也可以用于对象上,分配对象引用,如创建对象中所讨论的。

算术运算符

Java 编程语言提供了执行加法、减法、乘法和除法的运算符。您很可能会通过基本数学中的对应物来认识它们。唯一可能看起来新的符号是"%",它将一个操作数除以另一个操作数,并返回余数作为其结果。

运算符 描述
+ 加法运算符(也用于字符串连接)
- 减法运算符
* 乘法运算符
/ 除法运算符
% 取余运算符

下面的程序,ArithmeticDemo,测试了算术运算符。


class ArithmeticDemo {

    public static void main (String[] args) {

        int result = 1 + 2;
        // result is now 3
        System.out.println("1 + 2 = " + result);
        int original_result = result;

        result = result - 1;
        // result is now 2
        System.out.println(original_result + " - 1 = " + result);
        original_result = result;

        result = result * 2;
        // result is now 4
        System.out.println(original_result + " * 2 = " + result);
        original_result = result;

        result = result / 2;
        // result is now 2
        System.out.println(original_result + " / 2 = " + result);
        original_result = result;

        result = result + 8;
        // result is now 10
        System.out.println(original_result + " + 8 = " + result);
        original_result = result;

        result = result % 7;
        // result is now 3
        System.out.println(original_result + " % 7 = " + result);
    }
}

该程序打印如下内容:

1 + 2 = 3
3 - 1 = 2
2 * 2 = 4
4 / 2 = 2
2 + 8 = 10
10 % 7 = 3

您还可以将算术运算符与简单赋值运算符结合使用,创建复合赋值。例如,x+=1;x=x+1; 都会将 x 的值增加 1。

+ 运算符也可以用于连接(拼接)两个字符串,如下所示的ConcatDemo程序:


class ConcatDemo {
    public static void main(String[] args){
        String firstString = "This is";
        String secondString = " a concatenated string.";
        String thirdString = firstString+secondString;
        System.out.println(thirdString);
    }
}

到程序结束时,变量 thirdString 包含"这是一个连接的字符串。",并将其打印到标准输出。

一元运算符

一元运算符只需要一个操作数;它们执行各种操作,如将值增加/减少一,否定表达式,或反转布尔值的值。

运算符 描述
+ 一元加号运算符;表示正值(数字没有这个也是正的)
- 一元减号运算符;否定表达式
++ 递增运算符;将值增加 1
-- 递减运算符;将值减少 1
! 逻辑补运算符;反转布尔值的值

下面的程序,UnaryDemo,测试了一元运算符:


class UnaryDemo {

    public static void main(String[] args) {

        int result = +1;
        // result is now 1
        System.out.println(result);

        result--;
        // result is now 0
        System.out.println(result);

        result++;
        // result is now 1
        System.out.println(result);

        result = -result;
        // result is now -1
        System.out.println(result);

        boolean success = false;
        // false
        System.out.println(success);
        // true
        System.out.println(!success);
    }
}

递增/递减运算符可以在操作数之前(前缀)或之后(后缀)应用。代码 result++;++result; 都会使 result 增加一。唯一的区别在于前缀版本(++result)会计算为递增后的值,而后缀版本(result++)会计算为原始值。如果只是进行简单的递增/递减,选择哪个版本并不重要。但如果在较大表达式的一部分中使用此运算符,则您选择的版本可能会产生重大差异。

以下程序,PrePostDemo,演示了前缀/后缀一元递增运算符:


class PrePostDemo {
    public static void main(String[] args){
        int i = 3;
        i++;
        // prints 4
        System.out.println(i);
        ++i;			   
        // prints 5
        System.out.println(i);
        // prints 6
        System.out.println(++i);
        // prints 6
        System.out.println(i++);
        // prints 7
        System.out.println(i);
    }
}

相等、关系和条件运算符

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/op2.html

相等和关系运算符

相等和关系运算符确定一个操作数是否大于、小于、等于或不等于另一个操作数。这些运算符中的大多数可能对您来说看起来很熟悉。请记住,在测试两个原始值是否相等时,必须使用"==",而不是"="。

==      equal to
!=      not equal to
>       greater than
>=      greater than or equal to
<       less than
<=      less than or equal to

以下程序,ComparisonDemo,测试比较运算符:


class ComparisonDemo {

    public static void main(String[] args){
        int value1 = 1;
        int value2 = 2;
        if(value1 == value2)
            System.out.println("value1 == value2");
        if(value1 != value2)
            System.out.println("value1 != value2");
        if(value1 > value2)
            System.out.println("value1 > value2");
        if(value1 < value2)
            System.out.println("value1 < value2");
        if(value1 <= value2)
            System.out.println("value1 <= value2");
    }
}

输出:

value1 != value2
value1 <  value2
value1 <= value2

条件运算符

&&||运算符对两个布尔表达式执行条件-AND条件-OR操作。这些运算符表现出“短路”行为,这意味着只有在需要时才会评估第二个操作数。

&& Conditional-AND
|| Conditional-OR

以下程序,ConditionalDemo1,测试了这些运算符:


class ConditionalDemo1 {

    public static void main(String[] args){
        int value1 = 1;
        int value2 = 2;
        if((value1 == 1) && (value2 == 2))
            System.out.println("value1 is 1 AND value2 is 2");
        if((value1 == 1) || (value2 == 1))
            System.out.println("value1 is 1 OR value2 is 1");
    }
}

另一个条件运算符是?:,可以被视为if-then-else语句的简写(在本课程的控制流语句部分讨论)。这个运算符也被称为三元运算符,因为它使用三个操作数。在下面的例子中,这个运算符应该被理解为:“如果someConditiontrue,则将value1的值赋给result。否则,将value2的值赋给result。”

以下程序,ConditionalDemo2,测试了?:运算符:


class ConditionalDemo2 {

    public static void main(String[] args){
        int value1 = 1;
        int value2 = 2;
        int result;
        boolean someCondition = true;
        result = someCondition ? value1 : value2;

        System.out.println(result);
    }
}

因为someCondition为真,这个程序将"1"打印到屏幕上。如果使用?:运算符而不是if-then-else语句可以使您的代码更易读;例如,当表达式紧凑且没有副作用(如赋值)时。

类型比较运算符 instanceof

instanceof运算符将对象与指定类型进行比较。您可以使用它来测试对象是否是类的实例,子类的实例,或者实现特定接口的类的实例。

以下程序,InstanceofDemo,定义了一个父类(名为Parent),一个简单接口(名为MyInterface),以及一个继承自父类并实现接口的子类(名为Child)。


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

        Parent obj1 = new Parent();
        Parent obj2 = new Child();

        System.out.println("obj1 instanceof Parent: "
            + (obj1 instanceof Parent));
        System.out.println("obj1 instanceof Child: "
            + (obj1 instanceof Child));
        System.out.println("obj1 instanceof MyInterface: "
            + (obj1 instanceof MyInterface));
        System.out.println("obj2 instanceof Parent: "
            + (obj2 instanceof Parent));
        System.out.println("obj2 instanceof Child: "
            + (obj2 instanceof Child));
        System.out.println("obj2 instanceof MyInterface: "
            + (obj2 instanceof MyInterface));
    }
}

class Parent {}
class Child extends Parent implements MyInterface {}
interface MyInterface {}

输出:

obj1 instanceof Parent: true
obj1 instanceof Child: false
obj1 instanceof MyInterface: false
obj2 instanceof Parent: true
obj2 instanceof Child: true
obj2 instanceof MyInterface: true

当使用instanceof运算符时,请记住null不是任何东西的实例。

位与位移操作符

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/op3.html

Java 编程语言还提供对整数类型执行位和位移操作的运算符。本节讨论的运算符很少使用。因此,它们的覆盖范围很简短;目的只是让您知道这些运算符存在。

一元位取反运算符"~"反转位模式;它可以应用于任何整数类型,使每个"0"变为"1",每个"1"变为"0"。例如,一个byte包含 8 位;将此运算符应用于位模式为"00000000"的值将使其模式变为"11111111"。

有符号左移位运算符"<<"将位模式向左移动,有符号右移位运算符">>"将位模式向右移动。位模式由左操作数给出,要移动的位置数由右操作数给出。无符号右移位运算符">>>"将零移入最左侧位置,而">>"后的最左侧位置取决于符号扩展。

位运算符&执行位按位与操作。

位运算符^执行位按位异或操作。

位运算符|执行位按位包含或操作。

以下程序,BitDemo,使用位与运算符将数字"2"打印到标准输出。


class BitDemo {
    public static void main(String[] args) {
        int bitmask = 0x000F;
        int val = 0x2222;
        // prints "2"
        System.out.println(val & bitmask);
    }
}

运算符概要

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/opsummary.html

以下快速参考总结了 Java 编程语言支持的运算符。

简单赋值运算符

=       Simple assignment operator

算术运算符

+       Additive operator (also used
        for String concatenation)
-       Subtraction operator
*       Multiplication operator
/       Division operator
%       Remainder operator

一元运算符

+       Unary plus operator; indicates
        positive value (numbers are 
        positive without this, however)
-       Unary minus operator; negates
        an expression
++      Increment operator; increments
        a value by 1
--      Decrement operator; decrements
        a value by 1
!       Logical complement operator;
        inverts the value of a boolean

相等性和关系运算符

==      Equal to
!=      Not equal to
>       Greater than
>=      Greater than or equal to
<       Less than
<=      Less than or equal to

条件运算符

&&      Conditional-AND
||      Conditional-OR
?:      Ternary (shorthand for 
        if-then-else statement)

类型比较运算符

instanceof      Compares an object to 
                a specified type 

位运算符和位移运算符

~       Unary bitwise complement
<<      Signed left shift
>>      Signed right shift
>>>     Unsigned right shift
&       Bitwise AND
^       Bitwise exclusive OR
|       Bitwise inclusive OR

问题和练习:运算符

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/QandE/questions_operators.html

问题

  1. 考虑以下代码片段。

    arrayOfInts[j] > arrayOfInts[j+1]
    
    

    代码包含哪些运算符?

  2. 考虑以下代码片段。

    int i = 10;
    int n = i++%5;
    
    
    1. 在代码执行后,in的值是多少?

    2. 如果不使用后缀递增运算符(i++),而是使用前缀版本(++i),in的最终值是多少?

  3. 要反转boolean的值,你会使用哪个运算符?

  4. 用于比较两个值的运算符是=还是==

  5. 解释以下代码示例:result = someCondition ? value1 : value2;

练习

  1. 修改以下程序以使用复合赋值:

    class ArithmeticDemo {
    
         public static void main (String[] args){
    
              int result = 1 + 2; // result is now 3
              System.out.println(result);
    
              result = result - 1; // result is now 2
              System.out.println(result);
    
              result = result * 2; // result is now 4
              System.out.println(result);
    
              result = result / 2; // result is now 2
              System.out.println(result);
    
              result = result + 8; // result is now 10
              result = result % 7; // result is now 3
              System.out.println(result);
         }
    }
    
    
  2. 在以下程序中,解释为什么值“6”连续打印两次:

    class PrePostDemo {
        public static void main(String[] args){
            int i = 3;
            i++;
            System.out.println(i);    // "4"
            ++i;                     
            System.out.println(i);    // "5"
            System.out.println(++i);  // "6"
            System.out.println(i++);  // "6"
            System.out.println(i);    // "7"
        }
    }
    
    

检查你的答案

表达式、语句和块

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/expressions.html

现在你已经了解了变量和运算符,是时候学习 表达式语句 了。运算符可以用于构建计算值的表达式;表达式是语句的核心组件;语句可以分组成块。

表达式

一个 表达式 是由变量、运算符和方法调用构成的构造,根据语言的语法构造,计算为单个值。你已经看到了表达式的示例,如下所示:

int **cadence = 0**;
**anArray[0] = 100**;
System.out.println(**"Element 1 at index 0: " + anArray[0]**);

int **result = 1 + 2**; // result is now 3
if (**value1 == value2**) 
    System.out.println(**"value1 == value2"**);

表达式返回的值的数据类型取决于表达式中使用的元素。表达式 cadence = 0 返回一个 int,因为赋值运算符返回与其左操作数相同数据类型的值;在这种情况下,cadence 是一个 int。从其他表达式中可以看到,表达式也可以返回其他类型的值,比如 booleanString

Java 编程语言允许你从各种较小的表达式构建复合表达式,只要表达式的一部分所需的数据类型与另一部分的数据类型匹配。以下是一个复合表达式的示例:


1 * 2 * 3

在这个特定的例子中,表达式的计算顺序并不重要,因为乘法的结果与顺序无关;无论你如何应用乘法,结果始终相同。然而,并非所有表达式都是如此。例如,下面的表达式根据你是先执行加法还是除法操作而得出不同的结果:

x + y / 100    // ambiguous

你可以使用平衡的括号(( 和 ))来明确指定表达式的计算方式。例如,为了使前面的表达式不含糊,你可以写成以下形式:


(x + y) / 100  // unambiguous, recommended

如果你不明确指定操作的执行顺序,操作的顺序将由表达式中使用的运算符分配的优先级决定。具有更高优先级的运算符首先计算。例如,除法运算符的优先级高于加法运算符。因此,以下两个语句是等价的:

x + y / 100 

x + (y / 100) // unambiguous, recommended

在编写复合表达式时,要明确指出哪些运算符应该首先计算,并用括号表示。这种做法使代码更易于阅读和维护。

语句

语句大致相当于自然语言中的句子。语句 形成一个完整的执行单元。以下类型的表达式可以通过在表达式末尾加上分号 (;) 来转换为语句。

  • 赋值表达式

  • 任何使用 ++-- 的情况

  • 方法调用

  • 对象创建表达式

这些语句被称为 表达语句。以下是一些表达语句的例子。

// assignment statement
aValue = 8933.234;
// increment statement
aValue++;
// method invocation statement
System.out.println("Hello World!");
// object creation statement
Bicycle myBike = new Bicycle();

除了表达语句外,还有另外两种语句:声明语句控制流语句声明语句 用于声明一个变量。你已经看过很多声明语句的例子了:

// declaration statement
double aValue = 8933.234;

最后,控制流语句 调节语句执行的顺序。你将在下一节学习有关控制流语句的内容,控制流语句。

一个 是在平衡大括号之间的零个或多个语句组成的组,可以在允许单个语句的任何地方使用。下面的例子,BlockDemo,演示了块的使用:

class BlockDemo {
     public static void main(String[] args) {
          boolean condition = true;
          if (condition) { // begin block 1
               System.out.println("Condition is true.");
          } // end block one
          else { // begin block 2
               System.out.println("Condition is false.");
          } // end block 2
     }
}

问题和练习:表达式、语句和块

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/QandE/questions_expressions.html

问题

  1. 运算符可用于构建 ___,计算值。

  2. 表达式是 ___ 的核心组件。

  3. 语句可以分组为 ___。

  4. 以下代码片段是 ___ 表达式的示例。

     1 * 2 * 3
    
    
  5. 语句在自然语言中大致相当于句子,但语句以 ___ 结尾,而不是句号。

  6. 块是在平衡 ___ 之间的零个或多个语句组成的组,可以在允许单个语句的任何地方使用。

练习

识别以下种类的表达语句:

  • aValue = 8933.234;

  • aValue++;

  • System.out.println("Hello World!");

  • Bicycle myBike = new Bicycle();

检查你的答案

控制流语句

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/flow.html

源文件中的语句通常按照它们出现的顺序从上到下执行。然而,控制流语句通过使用决策、循环和分支打破执行流程,使您的程序能够有条件地执行特定的代码块。本节描述了 Java 编程语言支持的决策语句(if-thenif-then-elseswitch)、循环语句(forwhiledo-while)和分支语句(breakcontinuereturn)。

if-thenif-then-else 语句

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/if.html

if-then 语句

if-then 语句是所有控制流语句中最基本的。它告诉你的程序仅当特定测试评估为 true 时才执行某个代码段。例如,Bicycle 类可以允许刹车减少自行车的速度,仅当自行车已经在运动中时。applyBrakes 方法的一个可能的实现如下:

void applyBrakes() {
    // the "if" clause: bicycle must be moving
    if (isMoving){ 
        // the "then" clause: decrease current speed
        currentSpeed--;
    }
}

如果这个测试评估为 false(意味着自行车没有在运动中),控制跳转到 if-then 语句的末尾。

此外,如果“then”子句只包含一个语句,则开头和结尾的大括号是可选的:

void applyBrakes() {
    // same as above, but without braces 
    if (isMoving)
        currentSpeed--;
}

决定何时省略大括号是个人品味的问题。省略它们可能会使代码更脆弱。如果稍后向“then”子句添加第二个语句,一个常见的错误是忘记添加新需要的大括号。编译器无法捕捉到这种错误;你只会得到错误的结果。

if-then-else 语句

if-then-else 语句在“if”子句评估为 false 时提供了执行的第二路径。你可以在 applyBrakes 方法中使用 if-then-else 语句,如果自行车在静止状态下刹车,就采取一些行动。在这种情况下,行动就是简单地打印一个错误消息,说明自行车已经停止了。

void applyBrakes() {
    if (isMoving) {
        currentSpeed--;
    } else {
        System.err.println("The bicycle has already stopped!");
    } 
}

以下程序,IfElseDemo,根据测试分数的值分配等级:90% 或以上为 A,80% 或以上为 B,依此类推。


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

        int testscore = 76;
        char grade;

        if (testscore >= 90) {
            grade = 'A';
        } else if (testscore >= 80) {
            grade = 'B';
        } else if (testscore >= 70) {
            grade = 'C';
        } else if (testscore >= 60) {
            grade = 'D';
        } else {
            grade = 'F';
        }
        System.out.println("Grade = " + grade);
    }
}

程序的输出是:

    Grade = C

你可能已经注意到 testscore 的值可以满足复合语句中的多个表达式:76 >= 7076 >= 60。然而,一旦条件满足,适当的语句就会被执行(grade = 'C';),剩余的条件就不会被评估。

switch 语句

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html

if-thenif-then-else 语句不同,switch 语句可以有多个可能的执行路径。switch 适用于 byteshortcharint 原始数据类型。它还适用于枚举类型(在 枚举类型 中讨论)、String 类以及包装某些原始类型的几个特殊类:CharacterByteShortInteger(在 数字和字符串 中讨论)。

以下代码示例,SwitchDemo,声明了一个名为 monthint,其值表示一个月份。该代码根据 month 的值使用 switch 语句显示月份的名称。


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

        int month = 8;
        String monthString;
        switch (month) {
            case 1:  monthString = "January";
                     break;
            case 2:  monthString = "February";
                     break;
            case 3:  monthString = "March";
                     break;
            case 4:  monthString = "April";
                     break;
            case 5:  monthString = "May";
                     break;
            case 6:  monthString = "June";
                     break;
            case 7:  monthString = "July";
                     break;
            case 8:  monthString = "August";
                     break;
            case 9:  monthString = "September";
                     break;
            case 10: monthString = "October";
                     break;
            case 11: monthString = "November";
                     break;
            case 12: monthString = "December";
                     break;
            default: monthString = "Invalid month";
                     break;
        }
        System.out.println(monthString);
    }
}

在这种情况下,August 被打印到标准输出。

switch 语句的主体称为switch 块switch 块中的语句可以带有一个或多个 casedefault 标签。switch 语句评估其表达式,然后执行所有匹配 case 标签后的语句。

你也可以使用 if-then-else 语句显示月份的名称:

int month = 8;
if (month == 1) {
    System.out.println("January");
} else if (month == 2) {
    System.out.println("February");
}
...  // and so on

决定是使用 if-then-else 语句还是 switch 语句取决于可读性和语句测试的表达式。if-then-else 语句可以测试基于值范围或条件的表达式,而 switch 语句仅基于单个整数、枚举值或 String 对象测试表达式。

break 语句是另一个有趣的点。每个 break 语句终止包含的 switch 语句。控制流继续执行 switch 块后的第一条语句。break 语句是必要的,因为没有它们,switch 块中的语句会穿透:匹配的 case 标签后的所有语句会按顺序执行,而不管后续 case 标签的表达式如何,直到遇到 break 语句。程序 SwitchDemoFallThrough 展示了在 switch 块中穿透的语句。该程序显示了对应整数 month 的月份以及该年后续的月份:


public class SwitchDemoFallThrough {

    public static void main(String[] args) {
        java.util.ArrayList<String> futureMonths =
            new java.util.ArrayList<String>();

        int month = 8;

        switch (month) {
            case 1:  futureMonths.add("January");
            case 2:  futureMonths.add("February");
            case 3:  futureMonths.add("March");
            case 4:  futureMonths.add("April");
            case 5:  futureMonths.add("May");
            case 6:  futureMonths.add("June");
            case 7:  futureMonths.add("July");
            case 8:  futureMonths.add("August");
            case 9:  futureMonths.add("September");
            case 10: futureMonths.add("October");
            case 11: futureMonths.add("November");
            case 12: futureMonths.add("December");
                     break;
            default: break;
        }

        if (futureMonths.isEmpty()) {
            System.out.println("Invalid month number");
        } else {
            for (String monthName : futureMonths) {
               System.out.println(monthName);
            }
        }
    }
}

这是代码的输出:

August
September
October
November
December

从技术上讲,最后的break并不是必需的,因为流程会跳出switch语句。建议使用break,这样修改代码会更容易,也更少出错。default部分处理了所有未被case部分显式处理的值。

以下代码示例,SwitchDemo2,展示了一个语句可以有多个case标签。该代码示例计算了特定月份的天数:


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

        int month = 2;
        int year = 2000;
        int numDays = 0;

        switch (month) {
            case 1: case 3: case 5:
            case 7: case 8: case 10:
            case 12:
                numDays = 31;
                break;
            case 4: case 6:
            case 9: case 11:
                numDays = 30;
                break;
            case 2:
                if (((year % 4 == 0) && 
                     !(year % 100 == 0))
                     || (year % 400 == 0))
                    numDays = 29;
                else
                    numDays = 28;
                break;
            default:
                System.out.println("Invalid month.");
                break;
        }
        System.out.println("Number of Days = "
                           + numDays);
    }
}

这是代码的输出:

Number of Days = 29

switch语句中使用字符串。

在 Java SE 7 及更高版本中,可以在switch语句的表达式中使用String对象。以下代码示例,StringSwitchDemo,根据名为monthString的值显示月份的数字:


public class StringSwitchDemo {

    public static int getMonthNumber(String month) {

        int monthNumber = 0;

        if (month == null) {
            return monthNumber;
        }

        switch (month.toLowerCase()) {
            case "january":
                monthNumber = 1;
                break;
            case "february":
                monthNumber = 2;
                break;
            case "march":
                monthNumber = 3;
                break;
            case "april":
                monthNumber = 4;
                break;
            case "may":
                monthNumber = 5;
                break;
            case "june":
                monthNumber = 6;
                break;
            case "july":
                monthNumber = 7;
                break;
            case "august":
                monthNumber = 8;
                break;
            case "september":
                monthNumber = 9;
                break;
            case "october":
                monthNumber = 10;
                break;
            case "november":
                monthNumber = 11;
                break;
            case "december":
                monthNumber = 12;
                break;
            default: 
                monthNumber = 0;
                break;
        }

        return monthNumber;
    }

    public static void main(String[] args) {

        String month = "August";

        int returnedMonthNumber =
            StringSwitchDemo.getMonthNumber(month);

        if (returnedMonthNumber == 0) {
            System.out.println("Invalid month");
        } else {
            System.out.println(returnedMonthNumber);
        }
    }
}

这段代码的输出是8

switch表达式中的String与与每个case标签关联的表达式进行比较,就好像使用了String.equals方法一样。为了使StringSwitchDemo示例接受任何大小写的月份,month被转换为小写(使用toLowerCase方法),并且所有与case标签关联的字符串都是小写的。

注意:此示例检查switch语句中的表达式是否为null。确保任何switch语句中的表达式不为 null,以防止抛出NullPointerException

while 和 do-while 语句

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/while.html

while语句在特定条件为true时持续执行一组语句。其语法可以表示为:

while (expression) {
     statement(s)
}

while语句评估表达式,该表达式必须返回一个boolean值。如果表达式评估为truewhile语句执行while块中的语句while语句继续测试表达式并执行其块,直到表达式评估为false。使用while语句打印从 1 到 10 的值可以通过以下WhileDemo程序实现:


class WhileDemo {
    public static void main(String[] args){
        int count = 1;
        while (count < 11) {
            System.out.println("Count is: " + count);
            count++;
        }
    }
}

您可以使用while语句实现无限循环,如下所示:

while (true){
    // your code goes here
}

Java 编程语言还提供了do-while语句,可以表示如下:

do {
     statement(s)
} while (expression);

do-whilewhile之间的区别在于,do-while在循环底部评估其表达式,而不是在顶部。因此,do块内的语句始终至少执行一次,如下所示的DoWhileDemo程序中所示:


class DoWhileDemo {
    public static void main(String[] args){
        int count = 1;
        do {
            System.out.println("Count is: " + count);
            count++;
        } while (count < 11);
    }
}

for 语句

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/for.html

for语句提供了一种紧凑的方式来遍历一系列值。程序员通常将其称为"for 循环",因为它会重复循环,直到满足特定条件为止。for语句的一般形式可以表示如下:

for (*initialization*; *termination*;
     *increment*) {
    *statement(s)*
}

使用这个版本的for语句时,请记住:

  • 初始化表达式初始化循环;它在循环开始时执行一次。

  • 终止表达式评估为false时,循环终止。

  • 增量表达式在每次循环迭代之后被调用;这个表达式完全可以递增或递减一个值。

以下程序ForDemo使用for语句的一般形式将数字 1 到 10 打印到标准输出:


class ForDemo {
    public static void main(String[] args){
         for(int i=1; i<11; i++){
              System.out.println("Count is: " + i);
         }
    }
}

该程序的输出是:

Count is: 1
Count is: 2
Count is: 3
Count is: 4
Count is: 5
Count is: 6
Count is: 7
Count is: 8
Count is: 9
Count is: 10

请注意代码如何在初始化表达式中声明一个变量。该变量的作用域从其声明延伸到由for语句控制的块的末尾,因此它也可以在终止和增量表达式中使用。如果控制for语句的变量在循环外不需要,则最好在初始化表达式中声明该变量。通常使用ijk这些名称来控制for循环;在初始化表达式中声明它们会限制它们的生命周期并减少错误。

for循环的三个表达式是可选的;可以创建一个无限循环,如下所示:

// infinite loop
for ( ; ; ) {

    // your code goes here
}

for语句还有另一种形式,专为遍历集合和数组设计。这种形式有时被称为增强型 for语句,可使您的循环更加简洁和易读。为了演示,考虑以下数组,其中包含 1 到 10 的数字:

int[] numbers = {1,2,3,4,5,6,7,8,9,10};

以下程序EnhancedForDemo使用增强型for循环遍历数组:


class EnhancedForDemo {
    public static void main(String[] args){
         int[] numbers = 
             {1,2,3,4,5,6,7,8,9,10};
         for (int item : numbers) {
             System.out.println("Count is: " + item);
         }
    }
}

在这个例子中,变量item保存来自数字数组的当前值。该程序的输出与之前相同:

Count is: 1
Count is: 2
Count is: 3
Count is: 4
Count is: 5
Count is: 6
Count is: 7
Count is: 8
Count is: 9
Count is: 10

我们建议尽可能使用这种形式的for语句,而不是一般形式。

分支语句

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/branch.html

break 语句

break 语句有两种形式:带标签和未标记。在前面讨论 switch 语句时看到了未标记形式。您还可以使用未标记的 break 终止 forwhiledo-while 循环,如下所示的 BreakDemo 程序:

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

        int[] arrayOfInts = 
            { 32, 87, 3, 589,
              12, 1076, 2000,
              8, 622, 127 };
        int searchfor = 12;

        int i;
        boolean foundIt = false;

        for (i = 0; i < arrayOfInts.length; i++) {
            if (arrayOfInts[i] == searchfor) {
                foundIt = true;
                break;
            }
        }

        if (foundIt) {
            System.out.println("Found " + searchfor + " at index " + i);
        } else {
            System.out.println(searchfor + " not in the array");
        }
    }
}

该程序在数组中搜索数字 12。粗体显示的 break 语句在找到该值时终止 for 循环。然后控制流转移到 for 循环后的语句。该程序的输出为:

Found 12 at index 4

一个未标记的 break 语句终止最内层的 switchforwhiledo-while 语句,但带标签的 break 终止外部语句。下面的程序,BreakWithLabelDemo,类似于前一个程序,但使用嵌套的 for 循环在二维数组中搜索一个值。当找到该值时,带标签的 break 终止外部的 for 循环(标记为 "search"):


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

        int[][] arrayOfInts = { 
            { 32, 87, 3, 589 },
            { 12, 1076, 2000, 8 },
            { 622, 127, 77, 955 }
        };
        int searchfor = 12;

        int i;
        int j = 0;
        boolean foundIt = false;

    search:
        for (i = 0; i < arrayOfInts.length; i++) {
            for (j = 0; j < arrayOfInts[i].length;
                 j++) {
                if (arrayOfInts[i][j] == searchfor) {
                    foundIt = true;
                    break search;
                }
            }
        }

        if (foundIt) {
            System.out.println("Found " + searchfor + " at " + i + ", " + j);
        } else {
            System.out.println(searchfor + " not in the array");
        }
    }
}

这是程序的输出。

Found 12 at 1, 0

break 语句终止带标签的语句;它不会将控制流转移到标签处。控制流会转移到带标签的(终止的)语句之后的语句。

continue 语句

continue 语句跳过 forwhiledo-while 循环的当前迭代。未标记形式跳到最内层循环体的末尾并评估控制循环的 boolean 表达式。下面的程序,ContinueDemo,遍历一个 String,计算字母 "p" 的出现次数。如果当前字符不是 p,则 continue 语句跳过循环的其余部分并继续下一个字符。如果是 "p",程序会增加字母计数。


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

        String searchMe = "peter piper picked a " + "peck of pickled peppers";
        int max = searchMe.length();
        int numPs = 0;

        for (int i = 0; i < max; i++) {
            // interested only in p's
            if (searchMe.charAt(i) != 'p')
                continue;

            // process p's
            numPs++;
        }
        System.out.println("Found " + numPs + " p's in the string.");
    }
}

这是该程序的输出:

Found 9 p's in the string.

要更清楚地看到这种效果,请尝试删除 continue 语句并重新编译。再次运行程序时,计数将出错,显示找到了 35 个 p,而不是 9 个。

带标签的 continue 语句跳过带有给定标签的外部循环的当前迭代。下面的示例程序,ContinueWithLabelDemo,使用嵌套循环在另一个字符串中搜索子字符串。需要两个嵌套循环:一个用于迭代子字符串,一个用于迭代被搜索的字符串。下面的程序,ContinueWithLabelDemo,使用带标签的 continue 形式跳过外部循环的一个迭代。


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

        String searchMe = "Look for a substring in me";
        String substring = "sub";
        boolean foundIt = false;

        int max = searchMe.length() - 
                  substring.length();

    test:
        for (int i = 0; i <= max; i++) {
            int n = substring.length();
            int j = i;
            int k = 0;
            while (n-- != 0) {
                if (searchMe.charAt(j++) != substring.charAt(k++)) {
                    continue test;
                }
            }
            foundIt = true;
                break test;
        }
        System.out.println(foundIt ? "Found it" : "Didn't find it");
    }
}

这是该程序的输出。

Found it

return 语句

分支语句中的最后一个是return语句。return语句退出当前方法,控制流返回到调用方法的位置。return语句有两种形式:一种返回一个值,另一种不返回。要返回一个值,只需在return关键字后面放置该值(或计算该值的表达式)。

return ++count;

返回值的数据类型必须与方法声明的返回值类型匹配。当方法声明为void时,使用不返回值的return形式。

return;

类和对象课程将涵盖你需要了解的关于编写方法的一切。

控制流语句总结

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/flowsummary.html

if-then语句是所有控制流语句中最基本的。它告诉你的程序只有在特定测试评估为true时才执行某个代码段。if-then-else语句在“if”子句评估为false时提供了一个备用执行路径。与if-thenif-then-else不同,switch语句允许任意数量的执行路径。whiledo-while语句在特定条件为true时持续执行一系列语句。do-whilewhile之间的区别在于,do-while在循环底部评估其表达式而不是顶部。因此,do块内的语句至少会执行一次。for语句提供了一种紧凑的方式来迭代一系列值。它有两种形式,其中一种设计用于循环遍历集合和数组。

问题和练习:控制流语句

原文:docs.oracle.com/javase/tutorial/java/nutsandbolts/QandE/questions_flow.html

问题

  1. Java 编程语言支持的最基本的控制流语句是 ___ 语句。

  2. ___ 语句允许任意数量的可能执行路径。

  3. ___ 语句类似于while语句,但在循环的 ___ 处评估其表达式。

  4. 如何使用for语句编写一个无限循环?

  5. 如何使用while语句编写一个无限循环?

练习

  1. 考虑以下代码片段。

    if (aNumber >= 0)
        if (aNumber == 0)
            System.out.println("first string");
    else System.out.println("second string");
    System.out.println("third string");
    
    
    1. 如果aNumber为 3,你认为代码会产生什么输出?

    2. 编写一个包含上述代码片段的测试程序;将aNumber设为 3。程序的输出是什么?是否符合你的预期?解释输出为何是这样的;换句话说,代码片段的控制流是什么?

    3. 仅使用空格和换行符,重新格式化代码片段,使控制流更易于理解。

    4. 使用大括号,{ 和 },进一步澄清代码。

检查你的答案

课程:类和对象

原文:docs.oracle.com/javase/tutorial/java/javaOO/index.html

有了你现在对 Java 编程语言基础知识的了解,你可以学会编写自己的类。在本课程中,您将找到有关定义自己的类的信息,包括声明成员变量、方法和构造函数。

您将学会使用您的类来创建对象,以及如何使用您创建的对象。

本课程还涵盖了将类嵌套在其他类中以及枚举的内容

本节展示了类的结构,以及如何声明字段、方法和构造函数。

对象

本节介绍了创建和使用对象。您将学习如何实例化对象,以及一旦实例化,如何使用运算符访问对象的实例变量和方法。

更多关于类

本节涵盖了更多依赖于在前一节中学到的对象引用和运算符的类的方面:从方法返回值,this关键字,类与实例成员,以及访问控制。

嵌套类

静态嵌套类、内部类、匿名内部类、局部类和 lambda 表达式都有涵盖。还讨论了何时使用哪种方法。

枚举类型

本节介绍了枚举,这是一种特殊的类,允许您定义和使用一组常量。

原文:docs.oracle.com/javase/tutorial/java/javaOO/classes.html

在标题为面向对象编程概念的课程中,介绍了面向对象概念,以自行车类为例,其中赛车、山地车和双人车为子类。以下是一个可能实现Bicycle类的示例代码,让你了解类声明的概述。本课程的后续部分将逐步支持和解释类声明。暂时不要担心细节。

public class Bicycle {

    // the Bicycle class has
    // three *fields*
    public int cadence;
    public int gear;
    public int speed;

    // the Bicycle class has
    // one *constructor*
    public Bicycle(int startCadence, int startSpeed, int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;
    }

    // the Bicycle class has
    // four *methods*
    public void setCadence(int newValue) {
        cadence = newValue;
    }

    public void setGear(int newValue) {
        gear = newValue;
    }

    public void applyBrake(int decrement) {
        speed -= decrement;
    }

    public void speedUp(int increment) {
        speed += increment;
    }

}

MountainBike类的类声明,作为Bicycle的子类,可能如下所示:

public class MountainBike extends Bicycle {

    // the MountainBike subclass has
    // one *field*
    public int seatHeight;

    // the MountainBike subclass has
    // one *constructor*
    public MountainBike(int startHeight, int startCadence,
                        int startSpeed, int startGear) {
        super(startCadence, startSpeed, startGear);
        seatHeight = startHeight;
    }   

    // the MountainBike subclass has
    // one *method*
    public void setHeight(int newValue) {
        seatHeight = newValue;
    }   

}

MountainBike继承了Bicycle的所有字段和方法,并添加了seatHeight字段以及一个设置它的方法(山地车的座位可以根据地形要求上下移动)。

声明类

原文:docs.oracle.com/javase/tutorial/java/javaOO/classdecl.html

你已经看到以下方式定义的类:

class *MyClass* {
    // field, constructor, and 
    // method declarations
}

这是一个类声明类体(大括号之间的区域)包含了为从类创建的对象的生命周期提供支持的所有代码:用于初始化新对象的构造函数,提供类及其对象状态的字段声明,以及实现类及其对象行为的方法。

前面的类声明是一个最小的类声明。它只包含了类声明中所需的组件。你可以在类声明的开头提供关于类的更多信息,比如其超类的名称,是否实现了任何接口等。例如,

class *MyClass extends MySuperClass implements YourInterface* {
    // field, constructor, and
    // method declarations
}

意味着MyClassMySuperClass的子类,并且实现了YourInterface接口。

你也可以在最开始添加像publicprivate这样的修饰符—所以你可以看到类声明的开头行可能会变得相当复杂。决定其他类能否访问MyClass的修饰符publicprivate将在本课程的后面讨论。关于接口和继承的课程将解释在类声明中为什么以及如何使用extendsimplements关键字。目前你不需要担心这些额外的复杂性。

一般来说,类声明可以按顺序包括这些组件:

  1. 修饰符,比如publicprivate以及你以后会遇到的其他一些修饰符。(但是,请注意,private修饰符只能应用于嵌套类。)

  2. 类名,按照约定首字母大写。

  3. 类的父类(超类)的名称,如果有的话,前面带有关键字extends。一个类只能扩展(子类化)一个父类。

  4. 一个逗号分隔的接口列表,如果有的话,前面带有关键字implements。一个类可以实现多个接口。

  5. 类体,用大括号{}括起来。

声明成员变量

原文:docs.oracle.com/javase/tutorial/java/javaOO/variables.html

有几种类型的变量:

  • 类中的成员变量—这些被称为字段

  • 方法或代码块中的变量—这些被称为局部变量

  • 方法声明中的变量—这些被称为参数

Bicycle 类使用以下代码行来定义其字段:

public int cadence;
public int gear;
public int speed;

字段声明由三个组件组成,顺序为:

  1. 零个或多个修饰符,比如publicprivate

  2. 字段的类型。

  3. 字段的名称。

Bicycle 的字段名为cadencegearspeed,都是整数类型(int)。public关键字将这些字段标识为公共成员,可被任何可以访问该类的对象访问。

访问修饰符

使用的第一个(最左边的)修饰符让你控制其他类对成员字段的访问权限。目前,只考虑publicprivate。其他访问修饰符将在后面讨论。

  • public修饰符—该字段可从所有类访问。

  • private修饰符—该字段只能在其自身类中访问。

符合封装原则,通常会将字段设为私有。这意味着它们只能从Bicycle直接访问。然而,我们仍然需要访问这些值。这可以通过添加获取字段值的公共方法间接完成:

public class Bicycle {

    private int cadence;
    private int gear;
    private int speed;

    public Bicycle(int startCadence, int startSpeed, int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;
    }

    public int getCadence() {
        return cadence;
    }

    public void setCadence(int newValue) {
        cadence = newValue;
    }

    public int getGear() {
        return gear;
    }

    public void setGear(int newValue) {
        gear = newValue;
    }

    public int getSpeed() {
        return speed;
    }

    public void applyBrake(int decrement) {
        speed -= decrement;
    }

    public void speedUp(int increment) {
        speed += increment;
    }
}

类型

所有变量必须有一个类型。你可以使用原始类型如intfloatboolean等。或者你可以使用引用类型,比如字符串、数组或对象。

变量名

所有变量,无论是字段、局部变量还是参数,都遵循在语言基础课程中介绍的相同的命名规则和约定,变量—命名。

在这节课中,请注意方法和类名使用相同的命名规则和约定,除了

  • 类名的第一个字母应大写,而

  • 方法名的第一个(或唯一)单词应该是动词。

定义方法

原文:docs.oracle.com/javase/tutorial/java/javaOO/methods.html

这是一个典型方法声明的示例:

public double calculateAnswer(double wingSpan, int numberOfEngines,
                              double length, double grossTons) {
    //do the calculation here
}

方法声明的唯一必需元素是方法的返回类型、名称、一对括号(),以及大括号{}之间的方法体。

更一般地,方法声明有六个组成部分,顺序如下:

  1. 修饰符—例如publicprivate等,以及其他你将在后面学习的内容。

  2. 返回类型—方法返回的值的数据类型,或者如果方法不返回值,则为void

  3. 方法名称—字段名称的规则也适用于方法名称,但约定略有不同。

  4. 括号中的参数列表—以逗号分隔的输入参数列表,前面是它们的数据类型,用括号()括起来。如果没有参数,必须使用空括号。

  5. 一个异常列表—稍后讨论。

  6. 方法体,用大括号括起来—方法的代码,包括局部变量的声明,在这里。

修饰符、返回类型和参数将在本课程的后续部分讨论。异常将在后续课程中讨论。


定义: 方法声明的两个组成部分构成了方法签名—方法的名称和参数类型。


上面声明的方法的签名是:

calculateAnswer(double, int, double, double)

命名方法

尽管方法名称可以是任何合法标识符,但代码约定限制了方法名称。按照约定,方法名称应该是小写的动词或以小写动词开头的多词名称,后面跟着形容词、名词等。在多词名称中,第二个及后续单词的第一个字母应大写。以下是一些示例:

run
runFast
getBackground
getFinalData
compareTo
setX
isEmpty

通常,一个方法在其类中具有唯一的名称。但是,由于方法重载,一个方法可能与其他方法具有相同的名称。

方法重载

Java 编程语言支持方法重载,并且 Java 可以区分具有不同方法签名的方法。这意味着类中的方法如果具有不同的参数列表,则可以具有相同的名称(对此有一些限制,将在标题为“接口和继承”的课程中讨论)。

假设你有一个可以使用书法来绘制各种类型数据(字符串、整数等)的类,并且包含一个用于绘制每种数据类型的方法。为每个方法使用新名称很麻烦—例如,drawStringdrawIntegerdrawFloat等。在 Java 编程语言中,你可以为所有绘制方法使用相同的名称,但对每个方法传递不同的参数列表。因此,数据绘制类可能声明四个名为draw的方法,每个方法都有不同的参数列表。

public class DataArtist {
    ...
    public void draw(String s) {
        ...
    }
    public void draw(int i) {
        ...
    }
    public void draw(double f) {
        ...
    }
    public void draw(int i, double f) {
        ...
    }
}

过载方法通过传入方法的参数的数量和类型来区分。在代码示例中,draw(String s)draw(int i) 是不同且独特的方法,因为它们需要不同的参数类型。

你不能声明多个具有相同名称和相同数量及类型参数的方法,因为编译器无法区分它们。

编译器在区分方法时不考虑返回类型,因此即使它们具有不同的返回类型,也不能声明具有相同签名的两个方法。


注意: 过载方法应该谨慎使用,因为它们会使代码变得难以阅读。


为你的类提供构造函数

原文:docs.oracle.com/javase/tutorial/java/javaOO/constructors.html

一个类包含用于从类蓝图创建对象的构造函数。构造函数声明看起来像方法声明,只是它们使用类的名称并且没有返回类型。例如,Bicycle有一个构造函数:

public Bicycle(int startCadence, int startSpeed, int startGear) {
    gear = startGear;
    cadence = startCadence;
    speed = startSpeed;
}

要创建一个名为myBike的新Bicycle对象,需要通过new运算符调用构造函数:

Bicycle myBike = new Bicycle(30, 0, 8);

new Bicycle(30, 0, 8)在内存中为对象创建空间并初始化其字段。

尽管Bicycle只有一个构造函数,但它可以有其他构造函数,包括无参数构造函数:

public Bicycle() {
    gear = 1;
    cadence = 10;
    speed = 0;
}

Bicycle yourBike = new Bicycle();调用无参数构造函数以创建一个名为yourBike的新Bicycle对象。

由于它们具有不同的参数列表,两个构造函数都可以在Bicycle中声明。与方法一样,Java 平台根据参数列表中参数的数量和类型区分构造函数。你不能为同一个类编写具有相同数量和类型参数的两个构造函数,因为平台无法区分它们。这样做会导致编译时错误。

你不必为你的类提供任何构造函数,但在这样做时必须小心。编译器会自动为没有构造函数的任何类提供一个无参数默认构造函数。这个默认构造函数将调用超类的无参数构造函数。在这种情况下,如果超类没有无参数构造函数,编译器会报错,因此你必须验证它是否有。如果你的类没有显式的超类,那么它有一个隐式的超类Object,它有一个无参数构造函数。

你可以自己使用超类构造函数。本课程开头的MountainBike类就是这样做的。这将在后面关于接口和继承的课程中讨论。

你可以在构造函数的声明中使用访问修饰符来控制哪些其他类可以调用该构造函数。


注意:如果另一个类无法调用MyClass构造函数,则无法直接创建MyClass对象。


向方法或构造函数传递信息

原文:docs.oracle.com/javase/tutorial/java/javaOO/arguments.html

方法或构造函数的声明声明了该方法或构造函数的参数的数量和类型。例如,以下是一个计算房屋贷款月供的方法,基于贷款金额、利率、贷款期限(期数)和贷款的未来价值:

public double computePayment(
                  double loanAmt,
                  double rate,
                  double futureValue,
                  int numPeriods) {
    double interest = rate / 100.0;
    double partial1 = Math.pow((1 + interest), 
                    - numPeriods);
    double denominator = (1 - partial1) / interest;
    double answer = (-loanAmt / denominator)
                    - ((futureValue * partial1) / denominator);
    return answer;
}

该方法有四个参数:贷款金额、利率、未来价值和期数。前三个是双精度浮点数,第四个是整数。这些参数在方法体中被使用,并在运行时将采用传递的参数的值。


注意: 参数指的是方法声明中的变量列表。参数是在调用方法时传递的实际值。当您调用方法时,使用的参数必须与声明的参数在类型和顺序上匹配。


参数类型

您可以为方法或构造函数的参数使用任何数据类型。这包括原始数据类型,如双精度、浮点数和整数,就像您在computePayment方法中看到的那样,以及引用数据类型,如对象和数组。

这是一个接受数组作为参数的方法的示例。在这个示例中,该方法创建一个新的Polygon对象,并从一个Point对象数组中初始化它(假设Point是表示 x、y 坐标的类):

public Polygon polygonFrom(Point[] corners) {
    // method body goes here
}


注意:如果您想将一个方法传递给另一个方法,那么请使用 lambda 表达式或方法引用。


任意数量的参数

您可以使用称为varargs的构造来传递任意数量的值给方法。当您不知道将传递给方法的特定类型的参数有多少时,可以使用 varargs。这是一种快捷方式,可以手动创建数组(前一个方法可以使用 varargs 而不是数组)。

要使用 varargs,您需要在最后一个参数的类型后面加上省略号(三个点,...),然后是一个空格和参数名。然后该方法可以使用任意数量的该参数调用,包括零个。

public Polygon polygonFrom(Point... corners) {
    int numberOfSides = corners.length;
    double squareOfSide1, lengthOfSide1;
    squareOfSide1 = (corners[1].x - corners[0].x)
                     * (corners[1].x - corners[0].x) 
                     + (corners[1].y - corners[0].y)
                     * (corners[1].y - corners[0].y);
    lengthOfSide1 = Math.sqrt(squareOfSide1);

    // more method body code follows that creates and returns a 
    // polygon connecting the Points
}

您可以看到,在方法内部,corners被视为数组。该方法可以使用数组或一系列参数调用。方法体中的代码将在任何情况下将参数视为数组。

您最常见地会在打印方法中看到 varargs;例如,这个printf方法:

public PrintStream printf(String format, Object... args)

允许您打印任意数量的对象。可以这样调用:

System.out.printf("%s: %d, %s%n", name, idnum, address);

或者像这样

System.out.printf("%s: %d, %s, %s, %s%n", name, idnum, address, phone, email);

或者使用不同数量的参数。

参数名称

当您向方法或构造函数声明参数时,为该参数提供一个名称。此名称在方法体内用于引用传入的参数。

参数的名称在其作用域内必须是唯一的。它不能与同一方法或构造函数的另一个参数的名称相同,也不能与方法或构造函数内的局部变量的名称相同。

参数可以与类的字段之一具有相同的名称。如果是这种情况,则说参数遮蔽了字段。字段遮蔽可能会使您的代码难以阅读,并且通常仅在设置特定字段的构造函数和方法中使用。例如,考虑以下 Circle 类及其 setOrigin 方法:

public class Circle {
    private int x, y, radius;
    public void setOrigin(int x, int y) {
        ...
    }
}

Circle 类有三个字段:xyradiussetOrigin 方法有两个参数,每个参数的名称与一个字段的名称相同。每个方法参数都会遮蔽与其名称相同的字段。因此,在方法体内使用简单名称 xy 指的是参数,而不是字段。要访问字段,必须使用限定名称。这将在本课程的后面部分“使用 this 关键字”中讨论。

传递原始数据类型参数

原始参数,如 intdouble,是通过值传递给方法的。这意味着对参数值的任何更改仅存在于方法的范围内。当方法返回时,参数消失,对它们的任何更改都将丢失。以下是一个例子:

public class PassPrimitiveByValue {

    public static void main(String[] args) {

        int x = 3;

        // invoke passMethod() with 
        // x as argument
        passMethod(x);

        // print x to see if its 
        // value has changed
        System.out.println("After invoking passMethod, x = " + x);

    }

    // change parameter in passMethod()
    public static void passMethod(int p) {
        p = 10;
    }
}

运行此程序时,输出为:

After invoking passMethod, x = 3

传递引用数据类型参数

引用数据类型参数,如对象,也是通过值传递给方法的。这意味着当方法返回时,传入的引用仍然引用与之前相同的对象。但是,如果对象的字段值具有适当的访问级别,则可以在方法中更改对象的字段值。

例如,考虑一个在任意类中移动 Circle 对象的方法:

public void moveCircle(Circle circle, int deltaX, int deltaY) {
    // code to move origin of circle to x+deltaX, y+deltaY
    circle.setX(circle.getX() + deltaX);
    circle.setY(circle.getY() + deltaY);

    // code to assign a new reference to circle
    circle = new Circle(0, 0);
}

让方法使用这些参数被调用:

moveCircle(myCircle, 23, 56)

在方法内部,circle 最初指向 myCircle。该方法更改了 circle 引用的对象(即 myCircle)的 x 和 y 坐标分别为 23 和 56。这些更改将在方法返回时保留。然后 circle 被赋予一个新的 Circle 对象的引用,其中 x = y = 0。然而,这种重新赋值并不具有永久性,因为引用是通过值传递的,不能更改。在方法内部,circle 指向的对象已经改变,但是,当方法返回时,myCircle 仍然引用与调用方法之前相同的 Circle 对象。

对象

原文:docs.oracle.com/javase/tutorial/java/javaOO/objects.html

一个典型的 Java 程序会创建许多对象,正如您所知,这些对象通过调用方法进行交互。通过这些对象之间的交互,程序可以执行各种任务,比如实现 GUI、运行动画,或者在网络上传输和接收信息。一旦一个对象完成了它被创建的工作,它的资源就会被回收以供其他对象使用。

这里有一个小程序,名为CreateObjectDemo,它创建了三个对象:一个Point对象和两个Rectangle对象。您需要这三个源文件来编译这个程序。


public class CreateObjectDemo {

    public static void main(String[] args) {

        // Declare and create a point object and two rectangle objects.
        Point originOne = new Point(23, 94);
        Rectangle rectOne = new Rectangle(originOne, 100, 200);
        Rectangle rectTwo = new Rectangle(50, 100);

        // display rectOne's width, height, and area
        System.out.println("Width of rectOne: " + rectOne.width);
        System.out.println("Height of rectOne: " + rectOne.height);
        System.out.println("Area of rectOne: " + rectOne.getArea());

        // set rectTwo's position
        rectTwo.origin = originOne;

        // display rectTwo's position
        System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
        System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);

        // move rectTwo and display its new position
        rectTwo.move(40, 72);
        System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
        System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);
    }
}

该程序创建、操作并显示有关各种对象的信息。以下是输出:

Width of rectOne: 100
Height of rectOne: 200
Area of rectOne: 20000
X Position of rectTwo: 23
Y Position of rectTwo: 94
X Position of rectTwo: 40
Y Position of rectTwo: 72

以下三个部分使用上述示例来描述程序中对象的生命周期。通过它们,您将学习如何编写代码来在您自己的程序中创建和使用对象。您还将了解系统在对象的生命周期结束时如何清理。

创建对象

原文:docs.oracle.com/javase/tutorial/java/javaOO/objectcreation.html

正如你所知,一个类提供了对象的蓝图;你从一个类创建一个对象。以下语句取自 CreateObjectDemo 程序,每个语句创建一个对象并将其分配给一个变量:

Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);

第一行创建了一个 Point 类的对象,第二行和第三行分别创建了一个 Rectangle 类的对象。

这些语句中的每一个都有三个部分(下面详细讨论):

  1. 声明:加粗的代码都是将变量名与对象类型关联的变量声明。

  2. 实例化new 关键字是一个创建对象的 Java 操作符。

  3. 初始化new 操作符后跟一个构造函数调用,用于初始化新对象。

声明一个变量来引用一个对象

之前,你学到声明一个变量时,写的是:

*type name*;

这通知编译器,你将使用 name 来引用类型为 type 的数据。对于原始变量,这个声明也为变量保留了正确数量的内存。

你也可以单独在一行上声明一个引用变量。例如:

Point originOne;

如果你这样声明 originOne,它的值将在实际创建对象并分配给它之前是不确定的。仅仅声明一个引用变量并不会创建一个对象。为此,你需要使用 new 操作符,如下一节所述。在你的代码中使用 originOne 之前,你必须为其分配一个对象。否则,你将会得到一个编译器错误。

处于这种状态的变量,当前没有引用任何对象,可以用以下方式表示(变量名 originOne,加上指向空的引用):

originOne 为 null。

实例化一个类

new 操作符通过为新对象分配内存并返回对该内存的引用来实例化一个类。new 操作符还调用对象的构造函数。


注意:短语“实例化一个类”与“创建一个对象”意思相同。当你创建一个对象时,你正在创建一个类的“实例”,因此“实例化”一个类。


new 操作符需要一个后缀参数:一个构造函数调用。构造函数的名称提供了要实例化的类的名称。

new 操作符返回一个引用指向它创建的对象。这个引用通常赋给适当类型的变量,如:

Point originOne = new Point(23, 94);

new 操作符返回的引用不一定要赋给一个变量。它也可以直接在表达式中使用。例如:

int height = new Rectangle().height;

这个语句将在下一节讨论。

初始化一个对象

这是 Point 类的代码:

public class Point {
    public int x = 0;
    public int y = 0; //constructor
    public Point(int a, int b) {
        x = a;
        y = b;
    }
}

这个类包含一个构造函数。你可以通过构造函数的声明使用与类相同的名称且没有返回类型来识别构造函数。Point类中的构造函数接受两个整数参数,如代码(int a, int b)所声明。下面的语句为这些参数提供了值 23 和 94:

Point originOne = new Point(23, 94);

执行此语句的结果可以在下图中说明:

originOne 现在指向一个 Point 对象。

这是Rectangle类的代码,其中包含四个构造函数:

public class Rectangle {
    public int width = 0;
    public int height = 0;
    public Point origin;

    // four constructors
    public Rectangle() {
        origin = new Point(0, 0);
    }
    public Rectangle(Point p) {
        origin = p;
    }
    public Rectangle(int w, int h) {
        origin = new Point(0, 0);
        width = w;
        height = h;
    }
    public Rectangle(Point p, int w, int h) {
        origin = p;
        width = w;
        height = h;
    }

    // a method for moving the rectangle
    public void move(int x, int y) {
        origin.x = x;
        origin.y = y;
    }

    // a method for computing the area of the rectangle
    public int getArea() {
        return width * height;
    }
}

每个构造函数都允许你使用基本类型和引用类型为矩形的原点、宽度和高度提供初始值。如果一个类有多个构造函数,它们必须具有不同的签名。Java 编译器根据参数的数量和类型区分构造函数。当 Java 编译器遇到下面的代码时,它知道要调用Rectangle类中需要一个Point参数后跟两个整数参数的构造函数:


Rectangle rectOne = new Rectangle(originOne, 100, 200);

这调用了Rectangle的一个构造函数,将origin初始化为originOne。此外,构造函数将width设置为 100,height设置为 200。现在有两个指向同一个Point 对象的引用—一个对象可以有多个引用,如下图所示:

现在矩形的原点变量也指向了这个点。

下面的代码行调用了需要两个整数参数的Rectangle构造函数,这些参数为widthheight提供了初始值。如果你检查构造函数内的代码,你会看到它创建了一个新的Point对象,其xy值被初始化为 0:

Rectangle rectTwo = new Rectangle(50, 100);

下面语句中使用的Rectangle构造函数不带任何参数,因此被称为无参数构造函数

Rectangle rect = new Rectangle();

所有类至少有一个构造函数。如果一个类没有明确声明任何构造函数,Java 编译器会自动提供一个无参数构造函数,称为默认构造函数。这个默认构造函数调用类父类的无参数构造函数,或者如果类没有其他父类,则调用Object构造函数。如果父类没有构造函数(Object有一个),编译器会拒绝程序。

使用对象

译文:docs.oracle.com/javase/tutorial/java/javaOO/usingobject.html

创建对象后,您可能想要对其进行某些操作。您可能需要使用其中一个字段的值,更改其中一个字段,或调用其中一个方法执行操作。

引用对象的字段

通过它们的名称访问对象字段。您必须使用一个不含糊的名称。

您可以在其自身类中使用字段的简单名称。例如,我们可以在Rectangle类中添加一个语句并打印widthheight

System.out.println("Width and height are: " + width + ", " + height);

在这种情况下,widthheight是简单名称。

在对象的类之外的代码必须使用对象引用或表达式,后跟点(.)运算符,后跟一个简单的字段名称,如:

objectReference.fieldName

例如,CreateObjectDemo类中的代码位于Rectangle类的代码之外。因此,要引用Rectangle对象rectOne中的originwidthheight字段,CreateObjectDemo类必须分别使用名称rectOne.originrectOne.widthrectOne.height。程序使用这两个名称来显示rectOnewidthheight

System.out.println("Width of rectOne: "  + rectOne.width);
System.out.println("Height of rectOne: " + rectOne.height);

尝试在CreateObjectDemo类中的代码中使用简单名称widthheight是没有意义的 — 这些字段只存在于对象内部 — 并且会导致编译器错误。

稍后,程序使用类似的代码来显示有关rectTwo的信息。相同类型的对象具有自己的相同实例字段的副本。因此,每个Rectangle对象都有名为originwidthheight的字段。当您通过对象引用访问实例字段时,您引用特定对象的字段。CreateObjectDemo程序中的两个对象rectOnerectTwo具有不同的originwidthheight字段。

要访问字段,您可以使用一个命名引用对象,就像前面的例子中那样,或者您可以使用任何返回对象引用的表达式。请记住,new运算符返回一个对象的引用。因此,您可以使用从new返回的值来访问新对象的字段:

int height = new Rectangle().height;

此语句创建一个新的Rectangle对象并立即获取其高度。实质上,该语句计算了Rectangle的默认高度。请注意,在执行此语句后,程序不再具有对创建的Rectangle的引用,因为程序从未将引用存储在任何地方。该对象没有引用,其资源可以被 Java 虚拟机回收。

调用对象的方法

您还可以使用对象引用来调用对象的方法。您将方法的简单名称附加到对象引用上,中间使用点运算符(.)。此外,您可以在括号内提供方法的任何参数。如果方法不需要任何参数,请使用空括号。

objectReference.methodName(argumentList);

或者:

objectReference.methodName();

Rectangle类有两个方法:getArea()用于计算矩形的面积和move()用于改变矩形的原点。这是调用这两个方法的CreateObjectDemo代码:

System.out.println("Area of rectOne: " + rectOne.getArea());
...
rectTwo.move(40, 72);

第一条语句调用rectOnegetArea()方法并显示结果。第二行移动rectTwo,因为move()方法为对象的origin.xorigin.y赋予新值。

与实例字段一样,objectReference必须是一个对象的引用。你可以使用一个变量名,但也可以使用任何返回对象引用的表达式。new运算符返回一个对象引用,因此你可以使用从new返回的值来调用新对象的方法:

new Rectangle(100, 50).getArea()

表达式new Rectangle(100, 50)返回一个指向Rectangle对象的对象引用。如图所示,你可以使用点符号来调用新RectanglegetArea()方法来计算新矩形的面积。

一些方法,比如getArea(),会返回一个值。对于返回值的方法,你可以在表达式中使用方法调用。你可以将返回值赋给一个变量,用它做决策,或者控制一个循环。这段代码将getArea()返回的值赋给变量areaOfRectangle

int areaOfRectangle = new Rectangle(100, 50).getArea();

记住,在特定对象上调用方法就相当于向该对象发送消息。在这种情况下,getArea()被调用的对象是构造函数返回的矩形。

垃圾收集器

一些面向对象的语言要求你跟踪你创建的所有对象,并在不再需要时显式销毁它们。显式管理内存是繁琐且容易出错的。Java 平台允许你创建任意多的对象(当然,受系统处理能力的限制),而且你不必担心销毁它们。Java 运行时环境在确定不再使用对象时删除对象。这个过程称为垃圾收集

当没有更多引用指向对象时,对象就有资格进行垃圾收集。通常,变量中保存的引用在变量超出范围时会被丢弃。或者,你可以通过将变量设置为特殊值null来显式丢弃对象引用。记住,一个程序可以有多个引用指向同一个对象;在对象有资格进行垃圾收集之前,所有对对象的引用都必须被丢弃。

Java 运行时环境有一个垃圾收集器,定期释放不再被引用的对象使用的内存。当垃圾收集器确定时机合适时,它会自动执行其任务。

类的更多内容

原文:docs.oracle.com/javase/tutorial/java/javaOO/more.html

本节涵盖了更多与类相关的方面,这些方面依赖于在前面关于对象的章节中学到的对象引用和运算符:

  • 从方法中返回值。

  • this 关键字。

  • 类成员 vs. 实例成员。

  • 访问控制。

从方法返回一个值

原文:docs.oracle.com/javase/tutorial/java/javaOO/returnvalue.html

当方法达到return语句时返回到调用它的代码处

  • 完成方法中的所有语句,

  • 或者达到return语句,

  • 抛出异常(稍后讨论),

先发生的那个。

在方法声明中声明方法的返回类型。在方法体内,使用return语句返回值。

任何声明为void的方法不返回值。它不需要包含return语句,但可以包含。在这种情况下,return语句可用于跳出控制流块并退出方法,简单地像这样使用:

return;

如果尝试从声明为void的方法返回值,将会得到编译器错误。

任何未声明为void的方法必须包含一个带有相应返回值的return语句,就像这样:

return returnValue;

返回值的数据类型必须与方法声明的返回类型匹配;你不能从声明为返回布尔值的方法中返回整数值。

在关于对象的部分讨论的Rectangle Rectangle类中的getArea()方法返回一个整数:

    // a method for computing the area of the rectangle
    public int getArea() {
        return width * height;
    }

这个方法返回表达式width*height求值的整数。

getArea方法返回一个基本类型。一个方法也可以返回一个引用类型。例如,在一个操作Bicycle对象的程序中,我们可能有这样一个方法:

public Bicycle seeWhosFastest(Bicycle myBike, Bicycle yourBike,
                              Environment env) {
    Bicycle fastest;
    // code to calculate which bike is 
    // faster, given each bike's gear 
    // and cadence and given the 
    // environment (terrain and wind)
    return fastest;
}

返回一个类或接口

如果这一部分让你困惑,可以跳过它,在完成关于接口和继承的课程后再回来阅读。

当一个方法使用类名作为返回类型,比如whosFastest这样做时,返回对象的类型的类必须是返回类型的子类,或者是返回类型的确切类。假设你有一个类层次结构,其中ImaginaryNumberjava.lang.Number的子类,而java.lang.Number又是Object的子类,如下图所示。

ImaginaryNumber 的类层次结构

ImaginaryNumber 的类层次结构

现在假设你有一个声明为返回Number的方法:

public Number returnANumber() {
    ...
}

returnANumber方法可以返回一个ImaginaryNumber但不能返回一个ObjectImaginaryNumber是一个Number,因为它是Number的子类。然而,一个Object不一定是一个Number — 它可能是一个String或其他类型。

你可以重写一个方法并定义它返回原方法的子类,就像这样:

public ImaginaryNumber returnANumber() {
    ...
}

这种技术称为协变返回类型,意味着返回类型允许与子类相同方向变化。


注意: 你也可以使用接口名称作为返回类型。在这种情况下,返回的对象必须实现指定的接口。


使用 this 关键字

原文:docs.oracle.com/javase/tutorial/java/javaOO/thiskey.html

在实例方法或构造函数中,this是对当前对象的引用 — 即正在调用其方法或构造函数的对象。你可以通过使用this从实例方法或构造函数中引用当前对象的任何成员。

使用this与字段

使用this关键字的最常见原因是因为字段被方法或构造函数参数遮蔽。

例如,Point类是这样写的

public class Point {
    public int x = 0;
    public int y = 0;

    //constructor
    public Point(int a, int b) {
        x = a;
        y = b;
    }
}

但它也可以这样写:

public class Point {
    public int x = 0;
    public int y = 0;

    //constructor
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

每个构造函数的参数都会遮蔽对象的一个字段 — 在构造函数内部,x 是构造函数第一个参数的一个本地副本。要引用Point字段的x,构造函数必须使用this.x

使用this与构造函数

在构造函数内部,你也可以使用this关键字来调用同一类中的另一个构造函数。这样做被称为显式构造函数调用。这里有另一个Rectangle类,与 Objects 部分中的实现不同。

public class Rectangle {
    private int x, y;
    private int width, height;

    public Rectangle() {
        this(0, 0, 1, 1);
    }
    public Rectangle(int width, int height) {
        this(0, 0, width, height);
    }
    public Rectangle(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
    ...
}

该类包含一组构造函数。每个构造函数都初始化矩形的一些或全部成员变量。构造函数为任何未由参数提供初始值的成员变量提供默认值。例如,无参数构造函数在坐标 0,0 处创建一个 1x1 的Rectangle。两个参数的构造函数调用四个参数的构造函数,传入宽度和高度,但始终使用 0,0 坐标。与以前一样,编译器根据参数的数量和类型确定要调用的构造函数。

如果存在,调用另一个构造函数必须是构造函数中的第一行。

控制类的成员的访问

原文:docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html

访问级别修饰符确定其他类是否可以使用特定字段或调用特定方法。有两个访问控制级别:

  • 在顶层—public,或包私有(没有显式修饰符)。

  • 在成员级别—publicprivateprotected,或包私有(没有显式修饰符)。

一个类可以用修饰符public声明,这样该类对所有类都是可见的。如果一个类没有修饰符(默认情况,也称为包私有),它只在自己的包内可见(包是相关类的命名组 — 你将在后面的课程中了解它们。)

在成员级别,你也可以像顶级类一样使用public修饰符或无修饰符(包私有),意义相同。对于成员,还有两个额外的访问修饰符:privateprotectedprivate修饰符指定该成员只能在其自己的类中访问。protected修饰符指定该成员只能在其自己的包内访问(与包私有相同),并且可以由另一个包中其类的子类访问。

以下表格显示了每个修饰符允许的成员访问。

访问级别

修饰符 子类 全部
public Y Y Y Y
protected Y Y Y N
无修饰符 Y Y N N
private Y N N N

第一列数据表示类本身是否可以访问由访问级别定义的成员。正如你所看到的,类总是可以访问自己的成员。第二列表示与该类在同一包中的类(不考虑它们的父类)是否可以访问该成员。第三列表示在声明在此包之外的类的子类是否可以访问该成员。第四列表示所有类是否可以访问该成员。

访问级别以两种方式影响你。首先,当你使用来自其他来源的类时,例如 Java 平台中的类,访问级别确定你自己的类可以使用那些类的成员。其次,当你编写一个类时,你需要决定你的类中每个成员变量和每个方法应该具有什么访问级别。

让我们看一组类,并看看访问级别如何影响可见性。以下图显示了此示例中的四个类及其关系。

用于说明访问级别的示例中的类和包

用于说明访问级别的示例中的类和包

以下表格显示了可以应用于 Alpha 类的成员的每个访问修饰符的可见性。

可见性

修饰符 Alpha Beta Alphasub Gamma
public Y Y Y Y
protected Y Y Y N
无修饰符 Y Y N N
private Y N N N

选择访问级别的提示:

如果其他程序员使用您的类,您希望确保不会发生误用导致的错误。访问级别可以帮助您做到这一点。

  • 对于特定成员,使用最严格的访问级别是有意义的。除非有充分理由,否则使用private

  • 除了常量外,避免使用public字段。(教程中的许多示例使用公共字段。这可能有助于简洁地说明一些要点,但不建议在生产代码中使用。)公共字段往往会将您与特定实现联系起来,并限制您在更改代码时的灵活性。


理解类成员

原文:docs.oracle.com/javase/tutorial/java/javaOO/classvars.html

在本节中,我们讨论了使用static关键字来创建属于类而不是类实例的字段和方法。

类变量

当从同一类蓝图创建多个对象时,它们各自拥有自己独特的实例变量副本。在Bicycle类的情况下,实例变量是cadencegearspeed。每个Bicycle对象都有自己的这些变量的值,存储在不同的内存位置。

有时,你希望拥有对所有对象都通用的变量。这可以通过static修饰符来实现。在声明中具有static修饰符的字段称为静态字段类变量。它们与类关联,而不是与任何对象关联。类的每个实例共享一个类变量,它在内存中的一个固定位置。任何对象都可以更改类变量的值,但也可以在不创建类的实例的情况下操作类变量。

例如,假设你想创建多个Bicycle对象并为每个分配一个从 1 开始的序列号。这个 ID 号对每个对象是唯一的,因此是一个实例变量。同时,你需要一个字段来跟踪已创建多少Bicycle对象,以便知道要分配给下一个对象的 ID。这样的字段与任何单个对象无关,而是与整个类相关。为此,你需要一个类变量numberOfBicycles,如下所示:

public class Bicycle {

    private int cadence;
    private int gear;
    private int speed;

    // add an instance variable for the object ID
    private int id;

    // add a class variable for the
    // number of Bicycle objects instantiated
    private static int numberOfBicycles = 0;
        ...
}

类变量通过类名本身引用,如

Bicycle.numberOfBicycles

这样清楚地表明它们是类变量。


注意: 你也可以通过对象引用来引用静态字段

myBike.numberOfBicycles

但这是不被推荐的,因为它没有清楚地表明它们是类变量。


你可以使用Bicycle构造函数来设置id实例变量并递增numberOfBicycles类变量:

public class Bicycle {

    private int cadence;
    private int gear;
    private int speed;
    private int id;
    private static int numberOfBicycles = 0;

    public Bicycle(int startCadence, int startSpeed, int startGear){
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;

        // increment number of Bicycles
        // and assign ID number
        id = ++numberOfBicycles;
    }

    // new method to return the ID instance variable
    public int getID() {
        return id;
    }
        ...
}

类方法

Java 编程语言支持静态方法以及静态变量。具有static修饰符的静态方法应该使用类名调用,而无需创建类的实例,如

ClassName.methodName(args)


注意: 你也可以通过对象引用来引用静态方法

instanceName.methodName(args)

但这是不被推荐的,因为它没有清楚地表明它们是类方法。


静态方法的一个常见用途是访问静态字段。例如,我们可以向Bicycle类添加一个静态方法来访问numberOfBicycles静态字段:

public static int getNumberOfBicycles() {
    return numberOfBicycles;
}

并非所有实例和类变量和方法的组合都被允许:

  • 实例方法可以直接访问实例变量和实例方法。

  • 实例方法可以直接访问类变量和类方法。

  • 类方法可以直接访问类变量和类方法。

  • 类方法不能直接访问实例变量或实例方法—它们必须使用对象引用。此外,类方法不能使用this关键字,因为this没有实例可供参考。

常量

static修饰符与final修饰符结合使用,也用于定义常量。final修饰符表示此字段的值不能更改。

例如,以下变量声明定义了一个名为PI的常量,其值是圆周率的近似值(圆的周长与直径的比值):

static final double PI = 3.141592653589793;

以这种方式定义的常量不能被重新分配,如果程序尝试这样做,将在编译时出现错误。按照惯例,常量值的名称以大写字母拼写。如果名称由多个单词组成,则单词之间用下划线(_)分隔。


注意:如果原始类型或字符串被定义为常量,并且在编译时已知其值,则编译器会在代码中的所有位置用其值替换常量名称。这被称为编译时常量。如果常量在外部世界中的值发生变化(例如,如果立法规定 pi 实际上应该是 3.975),则需要重新编译使用此常量的任何类以获取当前值。


Bicycle

在本节中进行的所有修改后,Bicycle类现在是:

public class Bicycle {

    private int cadence;
    private int gear;
    private int speed;

    private int id;

    private static int numberOfBicycles = 0;

    public Bicycle(int startCadence,
                   int startSpeed,
                   int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;

        id = ++numberOfBicycles;
    }

    public int getID() {
        return id;
    }

    public static int getNumberOfBicycles() {
        return numberOfBicycles;
    }

    public int getCadence() {
        return cadence;
    }

    public void setCadence(int newValue) {
        cadence = newValue;
    }

    public int getGear(){
        return gear;
    }

    public void setGear(int newValue) {
        gear = newValue;
    }

    public int getSpeed() {
        return speed;
    }

    public void applyBrake(int decrement) {
        speed -= decrement;
    }

    public void speedUp(int increment) {
        speed += increment;
    }
}

初始化字段

原文:docs.oracle.com/javase/tutorial/java/javaOO/initial.html

正如你所见,你通常可以在声明中为字段提供初始值:

public class BedAndBreakfast {

    // initialize to 10
    public static int capacity = 10;

    // initialize to false
    private boolean full = false;
}

当初始化值可用且初始化可以放在一行时,这种方式效果很好。然而,这种初始化方式由于其简单性而有一些限制。如果初始化需要一些逻辑(例如,错误处理或使用for循环填充复杂数组),简单赋值是不够的。实例变量可以在构造函数中初始化,可以在那里使用错误处理或其他逻辑。为了为类变量提供相同的功能,Java 编程语言包括静态初始化块


注意:在类定义的开头声明字段并不是必需的,尽管这是最常见的做法。只需要在使用之前声明和初始化它们即可。


静态初始化块

静态初始化块是一个普通的代码块,用大括号{ }括起来,并在static关键字之前。这里是一个示例:

static {
    // whatever code is needed for initialization goes here
}

一个类可以有任意数量的静态初始化块,并且它们可以出现在类体的任何位置。运行时系统保证静态初始化块按照它们在源代码中出现的顺序调用。

还有一种替代静态块的方法 — 你可以编写一个私有静态方法:

class Whatever {
    public static varType myVar = initializeClassVariable();

    private static varType initializeClassVariable() {

        // initialization code goes here
    }
}

私有静态方法的优势在于,如果需要重新初始化类变量,它们可以在以后被重用。

初始化实例成员

通常,你会将代码放在构造函数中初始化实例变量。有两种替代方法可以用来初始化实例变量:初始化块和 final 方法。

实例变量的初始化块看起来就像静态初始化块,但没有static关键字:

{
    // whatever code is needed for initialization goes here
}

Java 编译器将初始化块复制到每个构造函数中。因此,这种方法可以用于在多个构造函数之间共享一段代码。

final 方法不能在子类中被重写。这在接口和继承的课程中有讨论。这里是使用 final 方法初始化实例变量的示例:

class Whatever {
    private varType myVar = initializeInstanceVariable();

    protected final varType initializeInstanceVariable() {

        // initialization code goes here
    }
}

如果子类可能希望重用初始化方法,则这是特别有用的。该方法是 final 的,因为在实例初始化期间调用非 final 方法可能会导致问题。

创建和使用类和对象摘要

原文:docs.oracle.com/javase/tutorial/java/javaOO/summaryclasses.html

类声明命名类并在大括号之间封装类体。类名可以由修饰符前置。类体包含类的字段、方法和构造函数。类使用字段来包含状态信息,并使用方法来实现行为。初始化类的新实例的构造函数使用类的名称,并且看起来像没有返回类型的方法。

您可以通过在声明中使用访问修饰符(如public)来以相同的方式控制对类和成员的访问。

通过在成员声明中使用static关键字来指定类变量或类方法。未声明为static的成员隐式地是实例成员。类变量由类的所有实例共享,并且可以通过类名以及实例引用访问。类的实例会获得每个实例变量的自己的副本,必须通过实例引用访问。

通过使用new运算符和构造函数,您可以从类创建对象。new运算符返回一个对创建的对象的引用。您可以将引用分配给变量或直接使用它。

可以通过使用限定名称来引用在声明它们的类之外的代码可访问的实例变量和方法。实例变量的限定名称如下所示:

*objectReference.variableName*

方法的限定名称如下所示:

*objectReference.methodName(argumentList)*

或:

*objectReference.methodName()*

垃圾收集器会自动清理未使用的对象。如果程序不再持有对对象的引用,则该对象将被视为未使用。您可以通过将持有引用的变量设置为null来显式丢弃引用。

问题和练习:类

原文:docs.oracle.com/javase/tutorial/java/javaOO/QandE/creating-questions.html

问题

  1. 考虑以下类:

    public class IdentifyMyParts {
        public static int x = 7; 
        public int y = 3; 
    }
    
    
    1. 类变量是什么?

    2. 实例变量是什么?

    3. 以下代码的输出是什么:

      IdentifyMyParts a = new IdentifyMyParts();
      IdentifyMyParts b = new IdentifyMyParts();
      a.y = 5;
      b.y = 6;
      a.x = 1;
      b.x = 2;
      System.out.println("a.y = " + a.y);
      System.out.println("b.y = " + b.y);
      System.out.println("a.x = " + a.x);
      System.out.println("b.x = " + b.x);
      System.out.println("IdentifyMyParts.x = " + IdentifyMyParts.x);
      
      

练习

  1. 编写一个类,其实例代表一副扑克牌中的一张牌。扑克牌有两个独特的属性:等级和花色。确保保留你的解决方案,因为你将被要求在枚举类型中重新编写它。


    提示:

    你可以使用assert语句来检查你的赋值。你可以写:

    assert (boolean expression to test); 
    
    

    如果布尔表达式为假,你将收到一个错误消息。例如,

    assert toString(ACE) == "Ace";
    
    

    应该返回true,这样就不会有错误消息。

    如果你使用assert语句,你必须用ea标志运行你的程序:

    java -ea YourProgram.class
    
    

  2. 编写一个类,其实例代表一副完整的扑克牌。你也应该保留这个解决方案。

  3. 3. 编写一个小程序来测试你的牌组和卡片类。这个程序可以简单到创建一副牌并显示其卡片。

检查你的答案。

问题和练习:对象

原文:docs.oracle.com/javase/tutorial/java/javaOO/QandE/objects-questions.html

问题

  1. 以下程序有什么问题?

    public class SomethingIsWrong {
        public static void main(String[] args) {
            Rectangle myRect;
            myRect.width = 40;
            myRect.height = 50;
            System.out.println("myRect's area is " + myRect.area());
        }
    }
    
    
  2. 以下代码创建了一个数组和一个字符串对象。在代码执行后,这些对象有多少个引用?这两个对象是否有资格进行垃圾回收?

    ...
    String[] students = new String[10];
    String studentName = "Peter Parker";
    students[0] = studentName;
    studentName = null;
    ...
    
    
  3. 程序如何销毁它创建的对象?

练习

  1. 修复问题 1 中显示的名为SomethingIsWrong的程序。

  2. 给定以下名为NumberHolder的类,编写一些代码来创建该类的一个实例,初始化其两个成员变量,然后显示每个成员变量的值。

    public class NumberHolder {
        public int anInt;
        public float aFloat;
    }
    
    

检查你的答案。

嵌套类

原文:docs.oracle.com/javase/tutorial/java/javaOO/nested.html

Java 编程语言允许您在另一个类中定义一个类。这样的类称为嵌套类,如下所示:

class OuterClass {
    ...
    class NestedClass {
        ...
    }
}


术语: 嵌套类分为两类:非静态和静态。非静态嵌套类称为内部类。声明为static的嵌套类称为静态嵌套类


class OuterClass {
    ...
    class InnerClass {
        ...
    }
    static class StaticNestedClass {
        ...
    }
}

嵌套类是其封闭类的成员。非静态嵌套类(内部类)可以访问封闭类的其他成员,即使它们被声明为 private。静态嵌套类无法访问封闭类的其他成员。作为OuterClass的成员,嵌套类可以声明为privatepublicprotected包私有。(请记住,外部类只能声明为public包私有。)

为什么使用嵌套类?

使用嵌套类的引人注目的原因包括以下内容:

  • 它是一种逻辑上将仅在一个地方使用的类分组的方法:如果一个类仅对另一个类有用,则将其嵌入该类并将两者保持在一起是合乎逻辑的。嵌套这样的“辅助类”使其包更加简洁。

  • 它增加了封装性:考虑两个顶层类 A 和 B,其中 B 需要访问 A 的成员,否则这些成员将被声明为private。通过将类 B 隐藏在类 A 中,A 的成员可以被声明为 private,并且 B 可以访问它们。此外,B 本身可以对外部世界隐藏。

  • 它可以导致更易读和易维护的代码:将小类嵌套在顶层类中可以使代码更接近其使用位置。

内部类

与实例方法和变量一样,内部类与其封闭类的实例相关联,并且可以直接访问该对象的方法和字段。此外,因为内部类与实例相关联,它本身不能定义任何静态成员。

作为内部类的实例存在于外部类的实例内部。考虑以下类:

class OuterClass {
    ...
    class InnerClass {
        ...
    }
}

InnerClass的实例只能存在于OuterClass的实例中,并且可以直接访问其封闭实例的方法和字段。

要实例化内部类,必须首先实例化外部类。然后,使用以下语法在外部对象中创建内部对象:

OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

有两种特殊类型的内部类:局部类和匿名类。

静态嵌套类

与类方法和变量一样,静态嵌套类与其外部类相关联。并且像静态类方法一样,静态嵌套类不能直接引用其封闭类中定义的实例变量或方法:它只能通过对象引用使用它们。内部类和嵌套静态类示例演示了这一点。


注意: 静态嵌套类与其外部类(以及其他类)的实例成员交互方式与任何其他顶层类相同。实际上,静态嵌套类在行为上就像是为了包装方便而嵌套在另一个顶层类中的顶层类。内部类和嵌套静态类示例也演示了这一点。


您可以像实例化顶层类一样实例化静态嵌套类:

StaticNestedClass staticNestedObject = new StaticNestedClass();

内部类和嵌套静态类示例

以下示例,OuterClass,以及TopLevelClass,演示了内部类(InnerClass)、嵌套静态类(StaticNestedClass)和顶层类(TopLevelClass)可以访问OuterClass的哪些类成员:

OuterClass.java


public class OuterClass {

    String outerField = "Outer field";
    static String staticOuterField = "Static outer field";

    class InnerClass {
        void accessMembers() {
            System.out.println(outerField);
            System.out.println(staticOuterField);
        }
    }

    static class StaticNestedClass {
        void accessMembers(OuterClass outer) {
            // Compiler error: Cannot make a static reference to the non-static
            //     field outerField
            // System.out.println(outerField);
            System.out.println(outer.outerField);
            System.out.println(staticOuterField);
        }
    }

    public static void main(String[] args) {
        System.out.println("Inner class:");
        System.out.println("------------");
        OuterClass outerObject = new OuterClass();
        OuterClass.InnerClass innerObject = outerObject.new InnerClass();
        innerObject.accessMembers();

        System.out.println("\nStatic nested class:");
        System.out.println("--------------------");
        StaticNestedClass staticNestedObject = new StaticNestedClass();        
        staticNestedObject.accessMembers(outerObject);

        System.out.println("\nTop-level class:");
        System.out.println("--------------------");
        TopLevelClass topLevelObject = new TopLevelClass();        
        topLevelObject.accessMembers(outerObject);                
    }
}

TopLevelClass.java


public class TopLevelClass {

    void accessMembers(OuterClass outer) {     
        // Compiler error: Cannot make a static reference to the non-static
        //     field OuterClass.outerField
        // System.out.println(OuterClass.outerField);
        System.out.println(outer.outerField);
        System.out.println(OuterClass.staticOuterField);
    }  
}

此示例打印以下输出:

Inner class:
------------
Outer field
Static outer field

Static nested class:
--------------------
Outer field
Static outer field

Top-level class:
--------------------
Outer field
Static outer field

请注意,静态嵌套类与其外部类的实例成员交互方式与任何其他顶层类相同。静态嵌套类StaticNestedClass无法直接访问outerField,因为它是封闭类OuterClass的实例变量。Java 编译器会在突出显示的语句处生成错误:

static class StaticNestedClass {
    void accessMembers(OuterClass outer) {
       // Compiler error: Cannot make a static reference to the non-static
       //     field outerField
       System.out.println(outerField);
    }
}

要修复此错误,请通过对象引用访问outerField

System.out.println(outer.outerField);

同样,顶层类TopLevelClass也无法直接访问outerField

遮蔽

如果特定范围(如内部类或方法定义)中的类型声明(如成员变量或参数名)与封闭范围中的另一个声明具有相同的名称,则声明会遮蔽封闭范围的声明。您不能仅通过名称引用被遮蔽的声明。以下示例,ShadowTest,演示了这一点:


public class ShadowTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
        }
    }

    public static void main(String... args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

以下是此示例的输出:

x = 23
this.x = 1
ShadowTest.this.x = 0

此示例定义了三个名为x的变量:类ShadowTest的成员变量,内部类FirstLevel的成员变量以及方法methodInFirstLevel中的参数。方法methodInFirstLevel中定义的变量x会遮蔽内部类FirstLevel的变量。因此,当您在方法methodInFirstLevel中使用变量x时,它指的是方法参数。要引用内部类FirstLevel的成员变量,请使用关键字this表示封闭范围:

System.out.println("this.x = " + this.x);

通过类名引用封装更大范围的成员变量。例如,以下语句从方法methodInFirstLevel中访问类ShadowTest的成员变量:

System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);

序列化

内部类的序列化,包括局部和匿名类,是强烈不建议的。当 Java 编译器编译某些结构(如内部类)时,它会创建合成结构;这些是在源代码中没有对应构造的类、方法、字段和其他结构。合成结构使 Java 编译器能够实现新的 Java 语言特性,而无需更改 JVM。然而,合成结构在不同的 Java 编译器实现之间可能会有所不同,这意味着.class文件在不同的实现之间也可能会有所不同。因此,如果您序列化一个内部类,然后在不同的 JRE 实现中反序列化它,可能会出现兼容性问题。有关在编译内部类时生成的合成结构的更多信息,请参见隐式和合成参数部分中的获取方法参数名称部分。

内部类示例

原文:docs.oracle.com/javase/tutorial/java/javaOO/innerclasses.html

要查看内部类的使用,请首先考虑一个数组。在以下示例中,您创建一个数组,填充它的整数值,然后仅按升序输出数组的偶数索引值。

接下来的DataStructure.java示例包括:

  • 包含构造函数以创建包含连续整数值(0、1、2、3 等)的数组的实例的DataStructure外部类,并且包含一个打印具有偶数索引值的数组元素的方法。

  • EvenIterator内部类,实现了DataStructureIterator接口,该接口扩展了Iterator<Integer>接口。迭代器用于遍历数据结构,通常具有用于测试最后一个元素、检索当前元素和移动到下一个元素的方法。

  • 一个main方法,实例化一个DataStructure对象(ds),然后调用printEven方法来打印具有偶数索引值的数组arrayOfInts的元素。


public class DataStructure {

    // Create an array
    private final static int SIZE = 15;
    private int[] arrayOfInts = new int[SIZE];

    public DataStructure() {
        // fill the array with ascending integer values
        for (int i = 0; i < SIZE; i++) {
            arrayOfInts[i] = i;
        }
    }

    public void printEven() {

        // Print out values of even indices of the array
        DataStructureIterator iterator = this.new EvenIterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
        System.out.println();
    }

    interface DataStructureIterator extends java.util.Iterator<Integer> { } 

    // Inner class implements the DataStructureIterator interface,
    // which extends the Iterator<Integer> interface

    private class EvenIterator implements DataStructureIterator {

        // Start stepping through the array from the beginning
        private int nextIndex = 0;

        public boolean hasNext() {

            // Check if the current element is the last in the array
            return (nextIndex <= SIZE - 1);
        }        

        public Integer next() {

            // Record a value of an even index of the array
            Integer retValue = Integer.valueOf(arrayOfInts[nextIndex]);

            // Get the next even element
            nextIndex += 2;
            return retValue;
        }
    }

    public static void main(String s[]) {

        // Fill the array with integer values and print out only
        // values of even indices
        DataStructure ds = new DataStructure();
        ds.printEven();
    }
}

输出为:

0 2 4 6 8 10 12 14 

请注意,EvenIterator类直接引用了DataStructure对象的arrayOfInts实例变量。

您可以使用内部类来实现辅助类,例如在此示例中所示的类。要处理用户界面事件,您必须知道如何使用内部类,因为事件处理机制广泛使用它们。

局部类和匿名类

有两种额外的内部类。您可以在方法体内声明一个内部类。这些类被称为局部类。您还可以在方法体内声明一个没有命名的内部类。这些类被称为匿名类。

修饰符

您可以为内部类使用与外部类的其他成员相同的修饰符。例如,您可以使用访问修饰符privatepublicprotected来限制对内部类的访问,就像您用它们来限制对其他类成员的访问一样。

本地类

原文:docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html

本地类是在中定义的类,块是在平衡大括号之间的零个或多个语句组成的组。通常在方法体中定义本地类。

本节涵盖以下主题:

  • 声明本地类

  • 访问封闭类的成员

    • 遮蔽和本地类
  • 本地类类似于内部类

声明本地类

您可以在任何块中定义本地类(请参阅表达式、语句和块了解更多信息)。例如,您可以在方法体、for循环或if子句中定义本地类。

以下示例,LocalClassExample,验证两个电话号码。它在方法validatePhoneNumber中定义了本地类PhoneNumber


public class LocalClassExample {

    static String regularExpression = "[⁰-9]";

    public static void validatePhoneNumber(
        String phoneNumber1, String phoneNumber2) {

        final int numberLength = 10;

        // Valid in JDK 8 and later:

        // int numberLength = 10;

        class PhoneNumber {

            String formattedPhoneNumber = null;

            PhoneNumber(String phoneNumber){
                // numberLength = 7;
                String currentNumber = phoneNumber.replaceAll(
                  regularExpression, "");
                if (currentNumber.length() == numberLength)
                    formattedPhoneNumber = currentNumber;
                else
                    formattedPhoneNumber = null;
            }

            public String getNumber() {
                return formattedPhoneNumber;
            }

            // Valid in JDK 8 and later:

//            public void printOriginalNumbers() {
//                System.out.println("Original numbers are " + phoneNumber1 +
//                    " and " + phoneNumber2);
//            }
        }

        PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
        PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);

        // Valid in JDK 8 and later:

//        myNumber1.printOriginalNumbers();

        if (myNumber1.getNumber() == null) 
            System.out.println("First number is invalid");
        else
            System.out.println("First number is " + myNumber1.getNumber());
        if (myNumber2.getNumber() == null)
            System.out.println("Second number is invalid");
        else
            System.out.println("Second number is " + myNumber2.getNumber());

    }

    public static void main(String... args) {
        validatePhoneNumber("123-456-7890", "456-7890");
    }
}

该示例通过首先从电话号码中删除除数字 0 到 9 之外的所有字符来验证电话号码。然后,它检查电话号码是否恰好包含十个数字(北美电话号码的长度)。该示例打印如下内容:

First number is 1234567890
Second number is invalid

访问封闭类的成员

本地类可以访问其封闭类的成员。在前面的示例中,PhoneNumber构造函数访问成员LocalClassExample.regularExpression

此外,本地类可以访问局部变量。但是,本地类只能访问声明为 final 的局部变量。当本地类访问封闭块的局部变量或参数时,它会捕获该变量或参数。例如,PhoneNumber构造函数可以访问局部变量numberLength,因为它声明为 final;numberLength是一个捕获的变量

然而,从 Java SE 8 开始,本地类可以访问封闭块中的局部变量和参数,这些变量是 final 或有效地 final。一旦初始化后值不会改变的变量或参数是有效地 final。例如,假设变量numberLength没有声明为 final,并且您在PhoneNumber构造函数中添加了突出显示的赋值语句以将有效电话号码的长度更改为 7 位:

PhoneNumber(String phoneNumber) {
    numberLength = 7;
    String currentNumber = phoneNumber.replaceAll(
        regularExpression, "");
    if (currentNumber.length() == numberLength)
        formattedPhoneNumber = currentNumber;
    else
        formattedPhoneNumber = null;
}

由于这个赋值语句,变量numberLength不再是有效地 final。因此,当内部类PhoneNumber尝试访问numberLength变量时,Java 编译器生成类似于"从内部类引用的局部变量必须是 final 或有效地 final"的错误消息:

if (currentNumber.length() == numberLength)

从 Java SE 8 开始,如果你在方法中声明局部类,它可以访问方法的参数。例如,你可以在PhoneNumber局部类中定义以下方法:

public void printOriginalNumbers() {
    System.out.println("Original numbers are " + phoneNumber1 +
        " and " + phoneNumber2);
}

方法printOriginalNumbers访问方法validatePhoneNumber的参数phoneNumber1phoneNumber2

遮蔽和局部类

在局部类中声明的类型(如变量)会遮蔽在外部作用域中具有相同名称的声明。更多信息请参见 Shadowing。

局部类类似于内部类

局部类类似于内部类,因为它们不能定义或声明任何静态成员。在静态方法中的局部类,比如在静态方法validatePhoneNumber中定义的PhoneNumber类,只能引用封闭类的静态成员。例如,如果你没有将成员变量regularExpression定义为静态的,那么 Java 编译器会生成类似“非静态变量regularExpression无法从静态上下文中引用”的错误。

局部类是非静态的,因为它们可以访问封闭块的实例成员。因此,它们不能包含大多数类型的静态声明。

你不能在块内部声明接口;接口本质上是静态的。例如,以下代码摘录不会编译,因为接口HelloThere是在方法greetInEnglish的主体内定义的:

    public void greetInEnglish() {
        interface HelloThere {
           public void greet();
        }
        class EnglishHelloThere implements HelloThere {
            public void greet() {
                System.out.println("Hello " + name);
            }
        }
        HelloThere myGreeting = new EnglishHelloThere();
        myGreeting.greet();
    }

你不能在局部类中声明静态初始化程序或成员接口。以下代码摘录不会编译,因为方法EnglishGoodbye.sayGoodbye被声明为static。当编译器遇到这个方法定义时,会生成类似“修饰符'static'仅允许在常量变量声明中使用”的错误:

    public void sayGoodbyeInEnglish() {
        class EnglishGoodbye {
            public static void sayGoodbye() {
                System.out.println("Bye bye");
            }
        }
        EnglishGoodbye.sayGoodbye();
    }

局部类可以拥有静态成员,前提是它们是常量变量。(常量变量是指声明为 final 并用编译时常量表达式初始化的原始类型或String类型的变量。编译时常量表达式通常是一个可以在编译时评估的字符串或算术表达式。更多信息请参见理解类成员。)以下代码摘录可以编译,因为静态成员EnglishGoodbye.farewell是一个常量变量:

    public void sayGoodbyeInEnglish() {
        class EnglishGoodbye {
            public static final String farewell = "Bye bye";
            public void sayGoodbye() {
                System.out.println(farewell);
            }
        }
        EnglishGoodbye myEnglishGoodbye = new EnglishGoodbye();
        myEnglishGoodbye.sayGoodbye();
    }

匿名类

原文:docs.oracle.com/javase/tutorial/java/javaOO/anonymousclasses.html

匿名类使您的代码更加简洁。它们使您能够同时声明和实例化一个类。它们类似于本地类,只是没有名称。如果您只需要使用本地类一次,请使用它们。

本节涵盖以下主题:

  • 声明匿名类

  • 匿名类的语法

  • 访问封闭范围的本地变量,并声明和访问匿名类的成员

  • 匿名类示例

声明匿名类

虽然本地类是类声明,匿名类是表达式,这意味着你在另一个表达式中定义类。以下示例,HelloWorldAnonymousClasses,在本地变量frenchGreetingspanishGreeting的初始化语句中使用了匿名类,但在变量englishGreeting的初始化中使用了本地类:


public class HelloWorldAnonymousClasses {

    interface HelloWorld {
        public void greet();
        public void greetSomeone(String someone);
    }

    public void sayHello() {

        class EnglishGreeting implements HelloWorld {
            String name = "world";
            public void greet() {
                greetSomeone("world");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hello " + name);
            }
        }

        HelloWorld englishGreeting = new EnglishGreeting();

        HelloWorld frenchGreeting = new HelloWorld() {
            String name = "tout le monde";
            public void greet() {
                greetSomeone("tout le monde");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Salut " + name);
            }
        };

        HelloWorld spanishGreeting = new HelloWorld() {
            String name = "mundo";
            public void greet() {
                greetSomeone("mundo");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hola, " + name);
            }
        };
        englishGreeting.greet();
        frenchGreeting.greetSomeone("Fred");
        spanishGreeting.greet();
    }

    public static void main(String... args) {
        HelloWorldAnonymousClasses myApp =
            new HelloWorldAnonymousClasses();
        myApp.sayHello();
    }            
}

匿名类的语法

如前所述,匿名类是一个表达式。匿名类表达式的语法类似于构造函数的调用,只是其中包含一个代码块中的类定义。

考虑frenchGreeting对象的实例化:

        HelloWorld frenchGreeting = new HelloWorld() {
            String name = "tout le monde";
            public void greet() {
                greetSomeone("tout le monde");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Salut " + name);
            }
        };

匿名类表达式包括以下内容:

  • new运算符

  • 要实现的接口名称或要扩展的类名称。在此示例中,匿名类正在实现接口HelloWorld

  • 包含传递给构造函数的参数的括号,就像普通的类实例创建表达式一样。注意:当您实现一个接口时,没有构造函数,所以您使用一个空的括号对,就像这个例子中一样。

  • 一个类声明体。更具体地说,在类体中,允许方法声明,但不允许语句。

因为匿名类定义是一个表达式,所以它必须是语句的一部分。在此示例中,匿名类表达式是实例化frenchGreeting对象的语句的一部分。(这就解释了为什么在右括号后有一个分号。)

访问封闭范围的本地变量,并声明和访问匿名类的成员

像本地类一样,匿名类可以捕获变量;它们对封闭范围的本地变量具有相同的访问权限:

  • 匿名类可以访问其封闭类的成员。

  • 匿名类无法访问其封闭范围中未声明为final或有效final的本地变量。

  • 像嵌套类一样,在匿名类中声明类型(如变量)会遮蔽封闭范围中具有相同名称的任何其他声明。有关更多信息,请参阅遮蔽。

匿名类在成员方面与局部类具有相同的限制:

  • 您不能在匿名类中声明静态初始化程序或成员接口。

  • 一个匿名类可以有静态成员,只要它们是常量变量。

请注意,您可以在匿名类中声明以下内容:

  • 字段

  • 额外的方法(即使它们不实现超类型的任何方法)

  • 实例初始化程序

  • 局部类

然而,你不能在匿名类中声明构造函数。

匿名类示例

匿名类经常用于图形用户界面(GUI)应用程序。

考虑 JavaFX 示例HelloWorld.java(来自Hello World, JavaFX Style部分,取自Getting Started with JavaFX)。此示例创建一个包含Say 'Hello World'按钮的框架。匿名类表达式被突出显示:

import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class HelloWorld extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Hello World!");
        Button btn = new Button();
        btn.setText("Say 'Hello World'");
        btn.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

        StackPane root = new StackPane();
        root.getChildren().add(btn);
        primaryStage.setScene(new Scene(root, 300, 250));
        primaryStage.show();
    }
}

在此示例中,方法调用btn.setOnAction指定了当您选择Say 'Hello World'按钮时会发生什么。此方法需要一个EventHandler<ActionEvent>类型的对象。EventHandler<ActionEvent>接口只包含一个方法,即 handle。该示例使用匿名类表达式而不是使用新类来实现此方法。请注意,此表达式是传递给btn.setOnAction方法的参数。

因为EventHandler<ActionEvent>接口只包含一个方法,所以您可以使用 lambda 表达式代替匿名类表达式。有关更多信息,请参阅 Lambda 表达式部分。

匿名类非常适合实现包含两个或更多方法的接口。以下 JavaFX 示例来自自定义 UI 控件部分。突出显示的代码创建一个仅接受数字值的文本字段。它通过覆盖从TextInputControl类继承的replaceTextreplaceSelection方法,使用匿名类重新定义了TextField类的默认实现。

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class CustomTextFieldSample extends Application {

    final static Label label = new Label();

    @Override
    public void start(Stage stage) {
        Group root = new Group();
        Scene scene = new Scene(root, 300, 150);
        stage.setScene(scene);
        stage.setTitle("Text Field Sample");

        GridPane grid = new GridPane();
        grid.setPadding(new Insets(10, 10, 10, 10));
        grid.setVgap(5);
        grid.setHgap(5);

        scene.setRoot(grid);
        final Label dollar = new Label("$");
        GridPane.setConstraints(dollar, 0, 0);
        grid.getChildren().add(dollar);

        final TextField sum = new TextField() {
            @Override
            public void replaceText(int start, int end, String text) {
                if (!text.matches("[a-z, A-Z]")) {
                    super.replaceText(start, end, text); 
                }
                label.setText("Enter a numeric value");
            }

            @Override
            public void replaceSelection(String text) {
                if (!text.matches("[a-z, A-Z]")) {
                    super.replaceSelection(text);
                }
            }
        };

        sum.setPromptText("Enter the total");
        sum.setPrefColumnCount(10);
        GridPane.setConstraints(sum, 1, 0);
        grid.getChildren().add(sum);

        Button submit = new Button("Submit");
        GridPane.setConstraints(submit, 2, 0);
        grid.getChildren().add(submit);

        submit.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                label.setText(null);
            }
        });

        GridPane.setConstraints(label, 0, 1);
        GridPane.setColumnSpan(label, 3);
        grid.getChildren().add(label);

        scene.setRoot(grid);
        stage.show();
    }

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

Lambda 表达式

原文:docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

匿名类的一个问题是,如果您的匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能显得笨拙和不清晰。在这些情况下,通常您试图将功能作为参数传递给另一个方法,例如当某人单击按钮时应执行什么操作。Lambda 表达式使您能够做到这一点,将功能视为方法参数,或将代码视为数据。

前一节,匿名类,向您展示了如何实现一个没有名称的基类。尽管这通常比具有名称的类更简洁,但对于只有一个方法的类来说,即使是匿名类似乎也有点过多和繁琐。Lambda 表达式让您更简洁地表达单方法类的实例。

本节涵盖以下主题:

  • Lambda 表达式的理想使用情况

    • 方法 1:创建搜索符合一个特征的成员的方法

    • 方法 2:创建更通用的搜索方法

    • 方法 3:在本地类中指定搜索条件代码

    • 方法 4:在匿名类中指定搜索条件代码

    • 方法 5:使用 Lambda 表达式指定搜索条件代码

    • 方法 6:使用 Lambda 表达式与标准功能接口

    • 方法 7:在整个应用程序中使用 Lambda 表达式

    • 方法 8:更广泛地使用泛型

    • 方法 9:使用接受 Lambda 表达式作为参数的聚合操作

  • GUI 应用程序中的 Lambda 表达式

  • Lambda 表达式的语法

  • 访问封闭范围的局部变量

  • 目标类型

    • 目标类型和方法参数
  • 序列化

Lambda 表达式的理想使用情况

假设您正在创建一个社交网络应用程序。您希望创建一个功能,使管理员能够对满足特定条件的社交网络应用程序成员执行任何类型的操作,例如发送消息。以下表格详细描述了这种用例:

字段 描述
名称 对所选成员执行操作
主要执行者 管理员
前提条件 管理员已登录到系统。
后置条件 操作仅在符合指定条件的成员上执行。
主要成功场景
  1. 管理员指定要执行某个操作的成员的条件。

  2. 管理员指定对所选成员执行的操作。

  3. 管理员选择提交按钮。

  4. 系统找到所有符合指定条件的成员。

  5. 系统对所有匹配成员执行指定操作。

|

扩展 1a. 管理员在指定执行操作或选择提交按钮之前有选项预览符合指定条件的成员。
出现频率 一天中多次。

假设这个社交网络应用程序的成员由以下Person类表示:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }

    public void printPerson() {
        // ...
    }
}

假设您的社交网络应用程序的成员存储在List<Person>实例中。

本节从一个简单的方法开始处理这种用例。它通过本地和匿名类改进了这种方法,然后以使用 lambda 表达式的高效简洁方法结束。在示例RosterTest中找到本节描述的代码摘录。

方法 1:创建搜索符合一个特征的成员的方法

一个简单的方法是创建几种方法;每种方法搜索符合一个特征的成员,例如性别或年龄。以下方法打印比指定年龄更老的成员:

public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}

注意List是一个有序的Collection集合是将多个元素组合成单个单元的对象。集合用于存储、检索、操作和传递聚合数据。有关集合的更多信息,请参阅 Collections 教程。

这种方法可能会使您的应用程序变得脆弱,这是应用程序由于引入更新(如新数据类型)而无法工作的可能性。假设您升级了应用程序并更改了Person类的结构,使其包含不同的成员变量;也许该类使用不同的数据类型或算法记录和测量年龄。您将不得不重写大量 API 以适应这种变化。此外,这种方法是不必要地限制性的;例如,如果您想打印比某个年龄更年轻的成员会怎样?

方法 2:创建更通用的搜索方法

以下方法比printPersonsOlderThan更通用;它打印指定年龄范围内的成员:

public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}

如果您想打印指定性别的成员,或者指定性别和年龄范围的组合会怎样?如果您决定更改Person类并添加其他属性,例如关系状态或地理位置会怎样?尽管这种方法比printPersonsOlderThan更通用,但尝试为每个可能的搜索查询创建单独的方法仍可能导致脆弱的代码。您可以将指定要搜索的条件的代码与不同类分开。

方法 3:在本地类中指定搜索条件代码

以下方法打印符合您指定搜索条件的成员:

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

此方法检查roster参数中包含的每个Person实例是否满足CheckPerson参数tester中指定的搜索条件,方法是调用tester.test方法。如果tester.test方法返回true值,则在Person实例上调用printPersons方法。

要指定搜索条件,您需要实现CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

以下类通过为test方法指定实现来实现CheckPerson接口。该方法过滤符合美国选择性服务资格的成员:如果其Person参数是男性且年龄在 18 至 25 岁之间,则返回true值:

class CheckPersonEligibleForSelectiveService implements CheckPerson {
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
            p.getAge() >= 18 &&
            p.getAge() <= 25;
    }
}

要使用此类,您需要创建一个新实例并调用printPersons方法:

printPersons(
    roster, new CheckPersonEligibleForSelectiveService());

尽管这种方法不太脆弱——如果更改Person的结构,您不必重新编写方法——但仍然会有额外的代码:为应用程序中计划执行的每个搜索创建一个新接口和一个本地类。由于CheckPersonEligibleForSelectiveService实现了一个接口,您可以使用匿名类代替本地类,避免为每个搜索声明一个新类的需要。

方法 4:在匿名类中指定搜索条件代码

下面方法printPersons的一个参数是一个匿名类,用于过滤符合美国选择性服务资格的成员:即男性且年龄在 18 至 25 岁之间的成员:

printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

这种方法减少了所需的代码量,因为您不必为要执行的每个搜索创建一个新类。然而,考虑到CheckPerson接口仅包含一个方法,匿名类的语法很臃肿。在这种情况下,您可以使用 Lambda 表达式代替匿名类,如下一节所述。

方法 5:使用 Lambda 表达式指定搜索条件代码

CheckPerson接口是一个函数式接口。函数式接口是仅包含一个抽象方法的任何接口。(函数式接口可以包含一个或多个默认方法或静态方法。)因为函数式接口仅包含一个抽象方法,所以在实现它时可以省略该方法的名称。为此,您可以使用 Lambda 表达式,如下面方法调用中所示:

printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

有关如何定义 Lambda 表达式的语法,请参阅 Lambda 表达式的语法。

您可以使用标准的函数式接口来替代CheckPerson接口,从而进一步减少所需的代码量。

第六种方法:使用 Lambda 表达式与标准函数式接口

重新考虑CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

这是一个非常简单的接口。它是一个函数式接口,因为它只包含一个抽象方法。这个方法接受一个参数并返回一个boolean值。这个方法如此简单,以至于在你的应用程序中定义一个可能不值得。因此,JDK 定义了几个标准的函数式接口,你可以在java.util.function包中找到。

例如,你可以在CheckPerson的位置使用Predicate<T>接口。这个接口包含方法boolean test(T t)

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

接口Predicate<T>是一个泛型接口的示例。(有关泛型的更多信息,请参阅泛型(更新)课程。)泛型类型(如泛型接口)在尖括号(<>)内指定一个或多个类型参数。这个接口只包含一个类型参数T。当你声明或实例化一个带有实际类型参数的泛型类型时,你就有了一个参数化类型。例如,参数化类型Predicate<Person>如下所示:

interface Predicate<Person> {
    boolean test(Person t);
}

这个参数化类型包含一个与CheckPerson.boolean test(Person p)具有相同返回类型和参数的方法。因此,你可以像下面的方法演示的那样使用Predicate<T>来替代CheckPerson

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

因此,下面的方法调用与你在第 3 种方法:在本地类中指定搜索条件代码中调用printPersons以获取符合选择性服务资格的成员时是相同的:

printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

这个方法中使用 Lambda 表达式的地方并不是唯一的。以下方法建议其他使用 Lambda 表达式的方式。

第七种方法:在整个应用程序中使用 Lambda 表达式

重新考虑printPersonsWithPredicate方法,看看还能在哪里使用 Lambda 表达式:

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

这个方法检查roster参数中包含的每个Person实例是否满足tester参数指定的条件。如果Person实例确实满足tester指定的条件,则在Person实例上调用printPerson方法。

你可以指定一个不同的操作来执行那些满足tester指定的条件的Person实例,而不是调用printPerson方法。你可以用 lambda 表达式指定这个操作。假设你想要一个类似于printPerson的 lambda 表达式,一个接受一个参数(一个Person类型的对象)并返回void的。记住,要使用 lambda 表达式,你需要实现一个函数式接口。在这种情况下,你需要一个包含可以接受一个Person类型参数并返回void的抽象方法的函数式接口。Consumer<T>接口包含方法void accept(T t),具有这些特征。以下方法用一个调用accept方法的Consumer<Person>实例替换了p.printPerson()的调用:

public static void processPersons(
    List<Person> roster,
    Predicate<Person> tester,
    Consumer<Person> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
}

因此,以下方法调用与在方法 3:在本地类中指定搜索条件代码中调用printPersons以获取符合应征条件的成员时是相同的。用于打印成员的 lambda 表达式被突出显示:

processPersons(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()
);

如果你想对成员的个人资料做更多操作而不仅仅是打印它们。假设你想验证成员的个人资料或检索他们的联系信息?在这种情况下,你需要一个包含返回值的抽象方法的函数式接口。Function<T,R>接口包含方法R apply(T t)。以下方法检索由参数mapper指定的数据,然后执行由参数block指定的操作:

public static void processPersonsWithFunction(
    List<Person> roster,
    Predicate<Person> tester,
    Function<Person, String> mapper,
    Consumer<String> block) {
    for (Person p : roster) {
        if (tester.test(p)) {
            String data = mapper.apply(p);
            block.accept(data);
        }
    }
}

以下方法从roster中包含的每个符合应征条件的成员中检索电子邮件地址,然后打印它:

processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

方法 8:更广泛地使用泛型

重新考虑processPersonsWithFunction方法。以下是一个通用版本,它接受一个包含任何数据类型元素的集合作为参数:

public static <X, Y> void processElements(
    Iterable<X> source,
    Predicate<X> tester,
    Function <X, Y> mapper,
    Consumer<Y> block) {
    for (X p : source) {
        if (tester.test(p)) {
            Y data = mapper.apply(p);
            block.accept(data);
        }
    }
}

要打印符合应征条件的成员的电子邮件地址,请按照以下方式调用processElements方法:

processElements(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

此方法调用执行以下操作:

  1. 从集合source中获取对象的源。在这个例子中,它从集合roster中获取Person对象的源。注意,集合roster是一个List类型的集合,也是一个Iterable类型的对象。

  2. 过滤与tester对象匹配的对象。在这个例子中,Predicate对象是一个指定哪些成员符合应征条件的 lambda 表达式。

  3. 将每个经过筛选的对象映射到由mapper对象指定的值。在这个例子中,Function对象是一个返回成员电子邮件地址的 lambda 表达式。

  4. 根据Consumer对象block指定的操作对每个映射对象执行动作。在此示例中,Consumer对象是一个打印字符串的 Lambda 表达式,该字符串是由Function对象返回的电子邮件地址。

您可以用聚合操作替换每个这些操作。

方法 9:使用接受 Lambda 表达式作为参数的聚合操作

以下示例使用聚合操作打印出集合roster中符合选择性服务资格的成员的电子邮件地址:

roster
    .stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(p -> p.getEmailAddress())
    .forEach(email -> System.out.println(email));

以下表格将方法processElements执行的每个操作与相应的聚合操作进行了映射:

processElements操作 聚合操作
获取对象源 Stream<E> **stream**()
过滤与Predicate对象匹配的对象 Stream<T> **filter**(Predicate<? super T> predicate)
根据Function对象将对象映射到另一个值 <R> Stream<R> **map**(Function<? super T,? extends R> mapper)
根据Consumer对象指定的操作执行动作 void **forEach**(Consumer<? super T> action)

操作filtermapforEach聚合操作。聚合操作处理来自流的元素,而不是直接来自集合(这就是为什么此示例中调用的第一个方法是stream的原因)。是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道从源(例如集合)传递值。管道是一系列流操作,本示例中是filter-map-forEach。此外,聚合操作通常接受 Lambda 表达式作为参数,使您能够自定义它们的行为。

对于更深入讨论聚合操作,请参阅聚合操作课程。

GUI 应用程序中的 Lambda 表达式

要处理图形用户界面(GUI)应用程序中的事件,例如键盘操作、鼠标操作和滚动操作,通常需要创建事件处理程序,这通常涉及实现特定的接口。通常,事件处理程序接口是函数式接口;它们往往只有一个方法。

在 JavaFX 示例HelloWorld.java(在上一节匿名类中讨论)中,您可以在此语句中用 Lambda 表达式替换突出显示的匿名类:

        btn.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

方法调用btn.setOnAction指定了当选择由btn对象表示的按钮时会发生什么。此方法需要一个EventHandler<ActionEvent>类型的对象。EventHandler<ActionEvent>接口只包含一个方法void handle(T event)。该接口是一个函数式接口,因此您可以使用以下突出显示的 Lambda 表达式来替换它:

        btn.setOnAction(
          event -> System.out.println("Hello World!")
        );

Lambda 表达式的语法

一个 lambda 表达式由以下内容组成:

  • 用括号括起的逗号分隔的形式参数列表。CheckPerson.test方法包含一个参数p,它表示Person类的一个实例。

    注意:您可以在 lambda 表达式中省略参数的数据类型。此外,如果只有一个参数,您可以省略括号。例如,以下 lambda 表达式也是有效的:

    p -> p.getGender() == Person.Sex.MALE 
        && p.getAge() >= 18
        && p.getAge() <= 25
    
  • 箭头标记,->

  • 一个由单个表达式或语句块组成的主体。本示例使用以下表达式:

    p.getGender() == Person.Sex.MALE 
        && p.getAge() >= 18
        && p.getAge() <= 25
    

    如果您指定一个单一表达式,那么 Java 运行时将评估该表达式,然后返回其值。或者,您可以使用一个返回语句:

    p -> {
        return p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25;
    }
    

    返回语句不是一个表达式;在 lambda 表达式中,您必须用大括号({})括起语句。然而,在 void 方法调用中,您不必用大括号括起。例如,以下是一个有效的 lambda 表达式:

    email -> System.out.println(email)
    

请注意,lambda 表达式看起来很像方法声明;您可以将 lambda 表达式视为匿名方法——没有名称的方法。

以下示例Calculator是一个使用多个形式参数的 lambda 表达式的示例:


public class Calculator {

    interface IntegerMath {
        int operation(int a, int b);   
    }

    public int operateBinary(int a, int b, IntegerMath op) {
        return op.operation(a, b);
    }

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

        Calculator myApp = new Calculator();
        IntegerMath addition = (a, b) -> a + b;
        IntegerMath subtraction = (a, b) -> a - b;
        System.out.println("40 + 2 = " +
            myApp.operateBinary(40, 2, addition));
        System.out.println("20 - 10 = " +
            myApp.operateBinary(20, 10, subtraction));    
    }
}

方法operateBinary对两个整数操作数执行数学运算。操作本身由IntegerMath的实例指定。该示例使用 lambda 表达式定义了两个操作,additionsubtraction。该示例打印如下内容:

40 + 2 = 42
20 - 10 = 10

访问封闭范围的局部变量

像局部类和匿名类一样,lambda 表达式可以捕获变量;它们对封闭范围的局部变量具有相同的访问权限。然而,与局部类和匿名类不同,lambda 表达式没有任何遮蔽问题(有关更多信息,请参见遮蔽)。Lambda 表达式是词法作用域的。这意味着它们不继承任何名称来自超类型,也不引入新的作用域级别。lambda 表达式中的声明被解释为在封闭环境中一样。以下示例LambdaScopeTest演示了这一点:


import java.util.function.Consumer;

public class LambdaScopeTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {

            int z = 2;

            Consumer<Integer> myConsumer = (y) -> 
            {
                // The following statement causes the compiler to generate
                // the error "Local variable z defined in an enclosing scope
                // must be final or effectively final" 
                //
                // z = 99;

                System.out.println("x = " + x); 
                System.out.println("y = " + y);
                System.out.println("z = " + z);
                System.out.println("this.x = " + this.x);
                System.out.println("LambdaScopeTest.this.x = " +
                    LambdaScopeTest.this.x);
            };

            myConsumer.accept(x);

        }
    }

    public static void main(String... args) {
        LambdaScopeTest st = new LambdaScopeTest();
        LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

本示例生成以下输出:

x = 23
y = 23
z = 2
this.x = 1
LambdaScopeTest.this.x = 0

如果在 lambda 表达式myConsumer的声明中,将参数x替换为y,那么编译器会生成一个错误:

Consumer<Integer> myConsumer = (x) -> {
    // ...
}

编译器生成错误“Lambda 表达式的参数 x 不能重新声明在封闭范围中定义的另一个局部变量”,因为 lambda 表达式不引入新的作用域级别。因此,可以直接访问封闭范围的字段、方法和局部变量。例如,lambda 表达式直接访问方法methodInFirstLevel的参数x。要访问封闭类中的变量,请使用关键字this。在这个例子中,this.x指的是成员变量FirstLevel.x

然而,与本地和匿名类一样,lambda 表达式只能访问封闭块的局部变量和参数,这些变量必须是 final 或有效 final。在这个例子中,变量z是有效 final;在初始化后其值不会改变。然而,假设在 lambda 表达式myConsumer中添加以下赋值语句:

Consumer<Integer> myConsumer = (y) -> {
    z = 99;
    // ...
}

由于这个赋值语句,变量z不再是有效 final。因此,Java 编译器生成类似于“定义在封闭范围中的局部变量 z 必须是 final 或有效 final”的错误消息。

目标类型

如何确定 lambda 表达式的类型?回想一下选择男性会员且年龄在 18 到 25 岁之间的 lambda 表达式:

p -> p.getGender() == Person.Sex.MALE
    && p.getAge() >= 18
    && p.getAge() <= 25

此 lambda 表达式在以下两个方法中使用:

  • 方法 3:在本地类中指定搜索条件代码 中的 public static void printPersons(List<Person> roster, CheckPerson tester)

  • 方法 6:使用标准函数接口和 Lambda 表达式 中的 public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)

当 Java 运行时调用方法 printPersons 时,它期望的数据类型是 CheckPerson,因此 lambda 表达式就是这种类型。然而,当 Java 运行时调用方法 printPersonsWithPredicate 时,它期望的数据类型是 Predicate<Person>,因此 lambda 表达式就是这种类型。这些方法期望的数据类型称为目标类型。为了确定 lambda 表达式的类型,Java 编译器使用 lambda 表达式所在上下文或情况的目标类型。由此可知,只能在 Java 编译器能够确定目标类型的情况下使用 lambda 表达式:

  • 变量声明

  • 赋值语句

  • 返回语句

  • 数组初始化器

  • 方法或构造函数参数

  • Lambda 表达式主体

  • 条件表达式,?:

  • 强制类型转换表达式

目标类型和方法参数

对于方法参数,Java 编译器使用另外两个语言特性来确定目标类型:重载解析和类型参数推断。

考虑以下两个函数式接口(java.lang.Runnablejava.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

方法Runnable.run不返回值,而Callable<V>.call返回值。

假设您已经重载了方法invoke如下(有关重载方法的更多信息,请参见定义方法):

void invoke(Runnable r) {
    r.run();
}

<T> T invoke(Callable<T> c) {
    return c.call();
}

在下面的语句中将调用哪个方法?

String s = invoke(() -> "done");

将调用方法invoke(Callable<T>),因为该方法返回一个值;方法invoke(Runnable)不返回值。在这种情况下,lambda 表达式() -> "done"的类型是Callable<T>

序列化

如果 lambda 表达式的目标类型和捕获的参数都是可序列化的,则可以对其进行序列化。然而,与内部类一样,强烈不建议序列化 lambda 表达式。

方法引用

原文:docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

你可以使用 lambda 表达式来创建匿名方法。然而,有时候 lambda 表达式仅仅是调用一个已存在的方法。在这种情况下,通过名称引用现有方法通常更清晰。方法引用使你能够做到这一点;它们是紧凑、易读的 lambda 表达式,用于已经有名称的方法。

再次考虑在 lambda 表达式部分讨论的Person类:

public class Person {

    // ...

    LocalDate birthday;

    public int getAge() {
        // ...
    }

    public LocalDate getBirthday() {
        return birthday;
    }   

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }

    // ...
}

假设你的社交网络应用的成员被包含在一个数组中,并且你想按年龄对数组进行排序。你可以使用以下代码(在示例MethodReferencesTest中找到本节描述的代码片段):

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}

Arrays.sort(rosterAsArray, new PersonAgeComparator());

此次调用sort的方法签名如下:

static <T> void sort(T[] a, Comparator<? super T> c)

注意Comparator接口是一个函数式接口。因此,你可以使用 lambda 表达式来代替定义并创建一个实现Comparator的类的新实例:

Arrays.sort(rosterAsArray,
    (Person a, Person b) -> {
        return a.getBirthday().compareTo(b.getBirthday());
    }
);

然而,比较两个Person实例的出生日期的方法Person.compareByAge已经存在。你可以在 lambda 表达式的主体中调用这个方法:

Arrays.sort(rosterAsArray,
    (a, b) -> Person.compareByAge(a, b)
);

因为这个 lambda 表达式调用了一个已存在的方法,你可以使用方法引用代替 lambda 表达式:

Arrays.sort(rosterAsArray, Person::compareByAge);

方法引用Person::compareByAge在语义上与 lambda 表达式(a, b) -> Person.compareByAge(a, b)相同。它们各自具有以下特征:

  • 其形式参数列表是从Comparator<Person>.compare复制的,即(Person, Person)

  • 其主体调用方法Person.compareByAge

方法引用的种类

有四种方法引用的种类:

种类 语法 示例
引用静态方法 *ContainingClass*::*staticMethodName* Person::compareByAge MethodReferencesExamples::appendStrings
引用特定对象的实例方法 *containingObject*::*instanceMethodName* myComparisonProvider::compareByName myApp::appendStrings2
引用特定类型的任意对象的实例方法 *ContainingType*::*methodName* String::compareToIgnoreCase String::concat
引用构造函数 *ClassName*::new HashSet::new

以下示例,MethodReferencesExamples,包含了前三种方法引用的示例:


import java.util.function.BiFunction;

public class MethodReferencesExamples {

    public static <T> T mergeThings(T a, T b, BiFunction<T, T, T> merger) {
        return merger.apply(a, b);
    }

    public static String appendStrings(String a, String b) {
        return a + b;
    }

    public String appendStrings2(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {

        MethodReferencesExamples myApp = new MethodReferencesExamples();

        // Calling the method mergeThings with a lambda expression
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", (a, b) -> a + b));

        // Reference to a static method
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", MethodReferencesExamples::appendStrings));

        // Reference to an instance method of a particular object        
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", myApp::appendStrings2));

        // Reference to an instance method of an arbitrary object of a
        // particular type
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", String::concat));
    }
}

所有的System.out.println()语句都打印相同的内容:Hello World!

BiFunctionjava.util.function包中许多函数接口之一。BiFunction函数接口可以表示接受两个参数并产生结果的 lambda 表达式或方法引用。

静态方法引用

方法引用Person::compareByAgeMethodReferencesExamples::appendStrings是对静态方法的引用。

引用特定对象的实例方法

下面是引用特定对象实例方法的示例:

class ComparisonProvider {
    public int compareByName(Person a, Person b) {
        return a.getName().compareTo(b.getName());
    }

    public int compareByAge(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);

方法引用myComparisonProvider::compareByName调用myComparisonProvider对象的compareByName方法。JRE 推断方法类型参数,本例中为(Person, Person)

类似地,方法引用myApp::appendStrings2将调用myApp对象的appendStrings2方法。JRE 推断方法类型参数,本例中为(String, String)

引用特定类型任意对象的实例方法

下面是一个引用特定类型任意对象的实例方法的示例:

String[] stringArray = { "Barbara", "James", "Mary", "John",
    "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

方法引用String::compareToIgnoreCase的等效 lambda 表达式将具有形式参数列表(String a, String b),其中ab是用于更好描述此示例的任意名称。方法引用将调用a.compareToIgnoreCase(b)方法。

类似地,方法引用String::concat将调用a.concat(b)方法。

构造函数引用

你可以通过使用名称new来引用构造函数,与引用静态方法的方式相同。以下方法将元素从一个集合复制到另一个集合:

public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(
        SOURCE sourceCollection,
        Supplier<DEST> collectionFactory) {

    DEST result = collectionFactory.get();
    for (T t : sourceCollection) {
        result.add(t);
    }
    return result;
}

函数接口Supplier包含一个名为get的方法,不接受参数并返回一个对象。因此,你可以使用 lambda 表达式调用方法transferElements,如下所示:

Set<Person> rosterSetLambda =
    transferElements(roster, () -> { return new HashSet<>(); });

你可以使用构造函数引用来替代 lambda 表达式,如下所示:

Set<Person> rosterSet = transferElements(roster, HashSet::new);

Java 编译器推断你想要创建一个包含类型为Person的元素的HashSet集合。或者,你可以按照以下方式指定:

Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);

何时使用嵌套类、局部类、匿名类和 Lambda 表达式

原文:docs.oracle.com/javase/tutorial/java/javaOO/whentouse.html

如在嵌套类一节中所述,嵌套类使您能够逻辑地将仅在一个地方使用的类分组,增加封装的使用,并创建更易读和可维护的代码。局部类、匿名类和 Lambda 表达式也具有这些优点;但是,它们旨在用于更具体的情况:

  • 局部类:如果需要创建一个类的多个实例、访问其构造函数或引入一个新的命名类型(例如,因为您需要稍后调用其他方法),请使用它。

  • 匿名类:如果需要声明字段或额外方法,请使用它。

  • Lambda 表达式:

    • 如果您要封装要传递给其他代码的单个行为单元,请使用它。例如,如果您希望对集合的每个元素执行某个操作,当进程完成时,或者当进程遇到错误时,您将使用 Lambda 表达式。

    • 如果需要一个功能接口的简单实例,并且前述条件均不适用(例如,您不需要构造函数、命名类型、字段或额外方法),请使用它。

  • 嵌套类:如果您的需求类似于局部类,并且希望使类型更广泛可用,且不需要访问局部变量或方法参数时,请使用它。

    • 如果需要访问封闭实例的非公共字段和方法,请使用非静态嵌套类(或内部类)。如果不需要此访问权限,请使用静态嵌套类。

问题和练习:嵌套类

原文:docs.oracle.com/javase/tutorial/java/javaOO/QandE/nested-questions.html

问题

  1. 程序Problem.java无法编译。你需要做什么才能使其编译?为什么?

  2. 使用 Java API 文档中Box类(位于javax.swing包中)的文档来帮助回答以下问题。

    1. Box定义了哪个静态嵌套类?

    2. Box定义了哪个内部类?

    3. Box的内部类的超类是什么?

    4. 从任何类中可以使用Box的哪些嵌套类?

    5. 如何创建BoxFiller类的实例?

练习

  1. 获取文件Class1.java。编译并运行Class1。输出是什么?

  2. 以下练习涉及修改类DataStructure.java,该类在内部类示例部分讨论。

    1. 定义一个名为print(DataStructureIterator iterator)的方法。使用EvenIterator类的实例调用此方法,使其执行与printEven方法相同的功能。

    2. 调用方法print(DataStructureIterator iterator),使其打印具有奇数索引值的元素。使用匿名类作为方法的参数,而不是接口DataStructureIterator的实例。

    3. 定义一个名为print(java.util.function.Function<Integer, Boolean> iterator)的方法,执行与print(DataStructureIterator iterator)相同的功能。使用 lambda 表达式调用此方法,以打印具有偶数索引值的元素。再次使用 lambda 表达式调用此方法,以打印具有奇数索引值的元素。

    4. 定义两个方法,使得以下两个语句打印具有偶数索引值和具有奇数索引值的元素:

      DataStructure ds = new DataStructure()
      // ...
      ds.print(DataStructure::isEvenIndex);
      ds.print(DataStructure::isOddIndex);
      

检查你的答案。

枚举类型

原文:docs.oracle.com/javase/tutorial/java/javaOO/enum.html

枚举类型是一种特殊的数据类型,允许变量成为一组预定义的常量之一。变量必须等于为其预定义的值之一。常见示例包括罗盘方向(NORTH、SOUTH、EAST 和 WEST 的值)和一周的天数。

由于它们是常量,枚举类型字段的名称必须是大写字母。

在 Java 编程语言中,您可以使用enum关键字定义枚举类型。例如,您可以指定一个星期几的枚举类型如下:


public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY 
}

每当需要表示一组固定常量时,都应该使用枚举类型。这包括自然枚举类型,如我们太阳系中的行星和在编译时知道所有可能值的数据集,例如菜单上的选项、命令行标志等。

这里是一些代码,向您展示如何使用上面定义的Day枚举:


public class EnumTest {
    Day day;

    public EnumTest(Day day) {
        this.day = day;
    }

    public void tellItLikeItIs() {
        switch (day) {
            case MONDAY:
                System.out.println("Mondays are bad.");
                break;

            case FRIDAY:
                System.out.println("Fridays are better.");
                break;

            case SATURDAY: case SUNDAY:
                System.out.println("Weekends are best.");
                break;

            default:
                System.out.println("Midweek days are so-so.");
                break;
        }
    }

    public static void main(String[] args) {
        EnumTest firstDay = new EnumTest(Day.MONDAY);
        firstDay.tellItLikeItIs();
        EnumTest thirdDay = new EnumTest(Day.WEDNESDAY);
        thirdDay.tellItLikeItIs();
        EnumTest fifthDay = new EnumTest(Day.FRIDAY);
        fifthDay.tellItLikeItIs();
        EnumTest sixthDay = new EnumTest(Day.SATURDAY);
        sixthDay.tellItLikeItIs();
        EnumTest seventhDay = new EnumTest(Day.SUNDAY);
        seventhDay.tellItLikeItIs();
    }
}

输出为:

Mondays are bad.
Midweek days are so-so.
Fridays are better.
Weekends are best.
Weekends are best.

Java 编程语言的枚举类型比其他语言中的对应类型更强大。enum声明定义了一个(称为枚举类型)。枚举类体可以包括方法和其他字段。编译器在创建枚举时会自动添加一些特殊方法。例如,它们具有一个静态values方法,返回一个包含枚举值的数组,按照它们声明的顺序排列。此方法通常与 for-each 结构结合使用,以遍历枚举类型的值。例如,下面Planet类示例中的代码遍历太阳系中的所有行星。

for (Planet p : Planet.values()) {
    System.out.printf("Your weight on %s is %f%n",
                      p, p.surfaceWeight(mass));
}


注意: 所有枚举隐式扩展java.lang.Enum。因为一个类只能扩展一个父类(参见声明类),Java 语言不支持状态的多重继承(参见状态、实现和类型的多重继承),因此枚举不能扩展其他任何内容。


在下面的示例中,Planet是一个表示太阳系行星的枚举类型。它们定义了常量质量和半径属性。

每个枚举常量都声明了质量和半径参数的值。这些值在创建常量时传递给构造函数。Java 要求常量在任何字段或方法之前定义。此外,当存在字段和方法时,枚举常量列表必须以分号结尾。


注意: 枚举类型的构造函数必须是包私有或私有访问。它会自动创建在枚举体开头定义的常量。您不能自己调用枚举构造函数。


除了其属性和构造函数外,Planet 还有一些方法,可以让你获取每个行星上物体的表面重力和重量。以下是一个示例程序,它接受你在地球上的体重(以任何单位)并计算并打印出你在所有行星上的体重(以相同单位):


public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    private double mass() { return mass; }
    private double radius() { return radius; }

    // universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight/EARTH.surfaceGravity();
        for (Planet p : Planet.values())
           System.out.printf("Your weight on %s is %f%n",
                             p, p.surfaceWeight(mass));
    }
}

如果你在命令行中运行 Planet.class 并带上参数 175,你会得到以下输出:

$ java Planet 175
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
Your weight on MARS is 66.279007
Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
Your weight on URANUS is 158.397260
Your weight on NEPTUNE is 199.207413

问题和练习:枚举类型

原文:docs.oracle.com/javase/tutorial/java/javaOO/QandE/enum-questions.html

问题

  1. 真或假:Enum类型可以是java.lang.String的子类。

练习

  1. 重写问题和练习:类中的Card类,使其使用枚举类型表示卡牌的等级和花色。

  2. 重写Deck类。

检查你的答案。

课程:注解

原文:docs.oracle.com/javase/tutorial/java/annotations/index.html

注解,一种元数据形式,提供关于程序的数据,这些数据不是程序本身的一部分。注解对其注释的代码的操作没有直接影响。

注解有多种用途,其中包括:

  • 编译器的信息 — 编译器可以使用注解来检测错误或抑制警告。

  • 编译时和部署时处理 — 软件工具可以处理注解信息以生成代码、XML 文件等。

  • 运行时处理 — 一些注解可以在运行时被检查。

本课程解释了注解可以在哪里使用,如何应用注解,在 Java 平台标准版(Java SE API)中有哪些预定义的注解类型可用,如何将类型注解与可插入类型系统结合使用以编写具有更强类型检查的代码,以及如何实现重复注解。

注解基础知识

原文:docs.oracle.com/javase/tutorial/java/annotations/basics.html

注解的格式

在其最简单的形式下,注解看起来像下面这样:

@Entity

符号@告诉编译器接下来是一个注解。在下面的例子中,注解的名称是Override

@Override
void mySuperMethod() { ... }

注解可以包括元素,这些元素可以是命名的或未命名的,并且这些元素有值:

@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass { ... }

@SuppressWarnings(value = "unchecked")
void myMethod() { ... }

如果只有一个名为value的元素,则名称可以省略,如:

@SuppressWarnings("unchecked")
void myMethod() { ... }

如果注解没有元素,则括号可以省略,如前面的@Override示例所示。

也可以在同一声明上使用多个注解:

@Author(name = "Jane Doe")
@EBook
class MyClass { ... }

如果注解具有相同的类型,则称为重复注解:

@Author(name = "Jane Doe")
@Author(name = "John Smith")
class MyClass { ... }

从 Java SE 8 发布开始支持重复注解。更多信息,请参见重复注解。

注解类型可以是 Java SE API 的java.langjava.lang.annotation包中定义的类型之一。在前面的示例中,OverrideSuppressWarnings是预定义的 Java 注解。还可以定义自己的注解类型。前面示例中的AuthorEbook注解是自定义注解类型。

注解可以使用的位置

注解可以应用于声明:类、字段、方法和其他程序元素的声明。当用于声明时,每个注解通常按照惯例出现在自己的一行上。

从 Java SE 8 发布开始,注解也可以应用于类型的使用。以下是一些示例:

  • 类实例创建表达式:

        new @Interned MyObject();
    
    
  • 类型转换:

        myString = (@NonNull String) str;
    
    
  • implements子句:

        class UnmodifiableList<T> implements
            @Readonly List<@Readonly T> { ... }
    
    
  • 抛出异常声明:

        void monitorTemperature() throws
            @Critical TemperatureException { ... }
    
    

这种形式的注解称为类型注解。更多信息,请参见类型注解和可插入类型系统。

声明注解类型

原文:docs.oracle.com/javase/tutorial/java/annotations/declaring.html

许多注解取代了代码中的注释。

假设一个软件组传统上在每个类的主体部分以提供重要信息的注释开头:

public class Generation3List extends Generation2List {

   // Author: John Doe
   // Date: 3/17/2002
   // Current revision: 6
   // Last modified: 4/12/2004
   // By: Jane Doe
   // Reviewers: Alice, Bill, Cindy

   // class code goes here

}

要使用注解添加相同的元数据,必须首先定义注解类型。这样做的语法是:

@interface ClassPreamble {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
   // Note use of array
   String[] reviewers();
}

注解类型定义看起来类似于接口定义,其中关键字interface之前有一个 at 符号(@)(@ = AT,表示注解类型)。注解类型是接口的一种形式,稍后会介绍。目前,您不需要理解接口。

前一个注解定义的主体包含注解类型元素声明,看起来很像方法。请注意,它们可以定义可选的默认值。

在定义注解类型之后,您可以像这样使用该类型的注解,填入值:

@ClassPreamble (
   author = "John Doe",
   date = "3/17/2002",
   currentRevision = 6,
   lastModified = "4/12/2004",
   lastModifiedBy = "Jane Doe",
   // Note array notation
   reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {

// class code goes here

}


注意: 要使@ClassPreamble中的信息出现在 Javadoc 生成的文档中,必须使用@Documented注解注释@ClassPreamble定义:

// import this to use @Documented
import java.lang.annotation.*;

@Documented
@interface ClassPreamble {

   // Annotation element definitions

}


预定义的注解类型

原文:docs.oracle.com/javase/tutorial/java/annotations/predefined.html

一组注解类型在 Java SE API 中预定义。一些注解类型由 Java 编译器使用,而一些适用于其他注解。

Java 语言使用的注解类型

java.lang中定义的预定义注解类型为@Deprecated@Override@SuppressWarnings

@Deprecated @Deprecated 注解表示标记的元素已被弃用,不应再使用。每当程序使用带有@Deprecated注解的方法、类或字段时,编译器都会生成警告。当元素被弃用时,还应使用 Javadoc 的@deprecated标签进行文档化,如下例所示。在 Javadoc 注释和注解中使用@符号并非巧合:它们在概念上是相关的。另外,请注意,Javadoc 标签以小写d开头,而注解以大写D开头。

   // Javadoc comment follows
    /**
     * *@deprecated*
     * *explanation of why it was deprecated*
     */
    @Deprecated
    static void deprecatedMethod() { }
}

@Override @Override 注解告诉编译器,该元素意在覆盖在超类中声明的元素。覆盖方法将在接口和继承中讨论。

   // *mark method as a superclass method*
   // *that has been overridden*
   @Override 
   int overriddenMethod() { }

虽然在覆盖方法时不是必须使用此注解,但它有助于防止错误。如果标记为@Override的方法未能正确覆盖其超类中的方法,编译器将生成错误。

@SuppressWarnings @SuppressWarnings 注解告诉编译器抑制其通常生成的特定警告。在下面的示例中,使用了一个已弃用的方法,编译器通常会生成警告。然而,在这种情况下,该注解导致警告被抑制。

   // *use a deprecated method and tell* 
   // *compiler not to generate a warning*
   @SuppressWarnings("deprecation")
    void useDeprecatedMethod() {
        // deprecation warning
        // - suppressed
        objectOne.deprecatedMethod();
    }

每个编译器警告都属于一个类别。Java 语言规范列出了两个类别:deprecationunchecked。当与在泛型出现之前编写的旧代码进行接口时,可能会出现unchecked警告。要抑制多个类别的警告,请使用以下语法:

@SuppressWarnings({"unchecked", "deprecation"})

@SafeVarargs @SafeVarargs 注解,当应用于方法或构造函数时,断言代码不对其varargs参数执行潜在不安全的操作。使用此注解类型时,与varargs使用相关的未经检查的警告将被抑制。

@FunctionalInterface @FunctionalInterface 注解,引入于 Java SE 8,指示类型声明旨在成为功能接口,如 Java 语言规范所定义。

适用于其他注解的注解

适用于其他注解的注解称为元注解。在java.lang.annotation中定义了几种元注解类型。

@Retention @Retention 注解指定标记的注解如何存储:

  • RetentionPolicy.SOURCE – 标记的注解仅在源级别保留,并被编译器忽略。

  • RetentionPolicy.CLASS – 标记的注解在编译时由编译器保留,但在 Java 虚拟机(JVM)中被忽略。

  • RetentionPolicy.RUNTIME – 标记的注解由 JVM 保留,因此可以被运行时环境使用。

@Documented @Documented 注解指示每当使用指定的注解时,应使用 Javadoc 工具记录这些元素。(默认情况下,注解不包含在 Javadoc 中。)有关更多信息,请参阅Javadoc 工具页面。

@Target @Target 注解标记另一个注解,限制注解可以应用于哪种 Java 元素。目标注解将以下元素类型之一指定为其值:

  • ElementType.ANNOTATION_TYPE 可以应用于注解类型。

  • ElementType.CONSTRUCTOR 可以应用于构造函数。

  • ElementType.FIELD 可以应用于字段或属性。

  • ElementType.LOCAL_VARIABLE 可以应用于局部变量。

  • ElementType.METHOD 可以应用于方法级别的注解。

  • ElementType.PACKAGE 可以应用于包声明。

  • ElementType.PARAMETER 可以应用于方法的参数。

  • ElementType.TYPE 可以应用于类的任何元素。

@Inherited @Inherited 注解指示注解类型可以从超类继承。(默认情况下不是这样。)当用户查询注解类型并且类没有此类型的注解时,将查询类的超类以获取注解类型。此注解仅适用于类声明。

@Repeatable @Repeatable 注解,引入于 Java SE 8,表示标记的注解可以多次应用于同一声明或类型使用。有关更多信息,请参阅重复注解。

类型注解和可插拔类型系统

原文:docs.oracle.com/javase/tutorial/java/annotations/type_annotations.html

在 Java SE 8 发布之前,注解只能应用于声明。从 Java SE 8 发布开始,注解也可以应用于任何类型使用。这意味着注解可以在使用类型的任何地方使用。一些类型使用的示例包括类实例创建表达式(new)、强制转换、implements子句和throws子句。这种形式的注解称为类型注解,注解基础知识提供了几个示例。

类型注解是为了支持改进 Java 程序的分析方式以确保更强的类型检查而创建的。Java SE 8 发布没有提供类型检查框架,但允许您编写(或下载)一个作为一个或多个可插拔模块实现的类型检查框架,这些模块与 Java 编译器一起使用。

例如,您希望确保程序中的特定变量永远不会被赋予 null;您希望避免触发NullPointerException。您可以编写一个自定义插件来检查这一点。然后,您将修改代码以注释该特定变量,指示它永远不会被赋予 null。变量声明可能如下所示:

@NonNull String str;

当您在命令行中编译代码时,包括NonNull模块,如果编译器检测到潜在问题,它会打印警告,让您修改代码以避免错误。在您纠正代码以消除所有警告后,当程序运行时,这种特定错误将不会发生。

您可以使用多个类型检查模块,每个模块检查不同类型的错误。通过这种方式,您可以在需要时在 Java 类型系统的基础上构建,添加特定的检查。

通过谨慎使用类型注解和存在可插拔类型检查器,您可以编写更强大且更不容易出错的代码。

在许多情况下,您不必编写自己的类型检查模块。有第三方已经为您完成了这项工作。例如,您可能希望利用华盛顿大学创建的检查器框架。该框架包括一个NonNull模块,以及一个正则表达式模块和一个互斥锁模块。更多信息,请参阅检查器框架。

可重复注解

原文:docs.oracle.com/javase/tutorial/java/annotations/repeating.html

有一些情况下,您希望将相同的注解应用于声明或类型使用。从 Java SE 8 发布开始,可重复注解使您能够做到这一点。

例如,您正在编写代码以使用一个定时器服务,该服务使您能够在指定时间运行一个方法或按照某个计划运行,类似于 UNIX 的cron服务。现在您想设置一个定时器在每个月的最后一天和每个星期五晚上 11 点运行一个doPeriodicCleanup方法。要设置定时器运行,创建一个@Schedule注解并将其应用两次于doPeriodicCleanup方法。第一次使用指定了每月的最后一天,第二次指定了星期五晚上 11 点,如下面的代码示例所示:

@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }

前面的示例对一个方法应用了一个注解。您可以在任何需要使用标准注解的地方重复使用注解。例如,您有一个处理未经授权访问异常的类。您为经理们注解了一个@Alert注解,为管理员注解了另一个:

@Alert(role="Manager")
@Alert(role="Administrator")
public class UnauthorizedAccessException extends SecurityException { ... }

由于兼容性原因,重复注解存储在 Java 编译器自动生成的容器注解中。为了让编译器做到这一点,您的代码中需要两个声明。

第 1 步:声明一个可重复的注解类型

注解类型必须标记为@Repeatable元注解。以下示例定义了一个自定义的@Schedule可重复注解类型:

import java.lang.annotation.Repeatable;

@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

@Repeatable元注解的值(括号中)是 Java 编译器生成的用于存储重复注解的容器注解的类型。在本例中,包含注解类型是Schedules,因此重复的@Schedule注解存储在一个@Schedules注解中。

在未声明为可重复之前将相同的注解应用于声明会导致编译时错误。

第 2 步:声明包含注解类型

包含注解类型必须具有一个带有数组类型的value元素。数组类型的组件类型必须是可重复注解类型。Schedules包含注解类型的声明如下:

public @interface Schedules {
    Schedule[] value();
}

检索注解

反射 API 中有几种可用的方法可用于检索注解。返回单个注解的方法的行为,例如AnnotatedElement.getAnnotation(Class),在只有一个请求类型的注解存在时保持不变。如果存在多个请求类型的注解,可以通过首先获取它们的容器注解来获取它们。通过这种方式,旧代码仍然可以正常工作。在 Java SE 8 中引入了其他方法,通过扫描容器注解一次返回多个注解,例如AnnotatedElement.getAnnotationsByType(Class)。请参阅AnnotatedElement类规范,了解所有可用方法的信息。

设计考虑事项

当设计注解类型时,您必须考虑该类型的注解的基数。现在可以使用一个注解零次,一次,或者,如果注解的类型标记为@Repeatable,可以使用多次。还可以通过使用@Target元注解来限制注解类型可以在哪里使用。例如,您可以创建一个可重复使用的注解类型,只能用于方法和字段。设计注解类型时要仔细考虑,以确保使用注解的程序员发现它尽可能灵活和强大。

问题和练习:注解

原文:docs.oracle.com/javase/tutorial/java/annotations/QandE/questions.html

问题

  1. 以下接口有什么问题?

    public interface House {
        @Deprecated
        void open();
        void openFrontDoor();
        void openBackDoor();
    }
    
    
  2. 考虑House接口的以下实现,如问题 1 所示。

    public class MyHouse implements House {
        public void open() {}
        public void openFrontDoor() {}
        public void openBackDoor() {}
    }
    
    

    如果您编译此程序,编译器会产生警告,因为open已被弃用(在接口中)。您可以采取什么措施消除该警告?

  3. 以下代码是否会编译出错?为什么?

    public @interface Meal { ... }
    
    @Meal("breakfast", mainDish="cereal")
    @Meal("lunch", mainDish="pizza")
    @Meal("dinner", mainDish="salad")
    public void evaluateDiet() { ... }
    
    

练习

  1. 为增强请求定义一个注解类型,具有idsynopsisengineerdate元素。为工程师指定默认值为unassigned,为日期指定默认值为unknown

检查您的答案。

课程:接口和继承

原文:docs.oracle.com/javase/tutorial/java/IandI/index.html

接口

在上一课中,您看到了实现接口的示例。您可以在这里阅读更多关于接口的信息——它们的作用是什么,为什么您可能想要编写一个,以及如何编写一个。

继承

这一部分描述了如何从一个类派生另一个类。也就是说,子类如何从超类继承字段和方法。您将了解到所有类都是从Object类派生的,以及如何修改子类从超类继承的方法。本节还涵盖类似接口的抽象类

接口

原文:docs.oracle.com/javase/tutorial/java/IandI/createinterface.html

在软件工程中有许多情况下,不同组的程序员需要达成一致的“合同”,明确规定他们的软件如何交互。每个组都应该能够编写他们的代码,而不需要了解其他组的代码是如何编写的。一般来说,接口就是这样的合同。

例如,想象一个未来社会,在这个社会中,由计算机控制的机器人汽车在城市街道上运载乘客,没有人类操作员。汽车制造商编写软件(当然是 Java),操作汽车—停止、启动、加速、左转等等。另一个工业团体,电子导航仪制造商,制造接收 GPS(全球定位系统)位置数据和交通状况无线传输的计算机系统,并利用这些信息驾驶汽车。

汽车制造商必须发布一个行业标准接口,详细说明可以调用哪些方法来使汽车移动(任何制造商的任何汽车)。导航制造商可以编写调用接口中描述的方法来命令汽车的软件。两个工业团体都不需要知道*对方的软件是如何实现的。事实上,每个团体都认为自己的软件是高度专有的,并保留随时修改的权利,只要它继续遵守已发布的接口。

Java 中的接口

在 Java 编程语言中,接口是一种引用类型,类似于类,只能包含常量、方法签名、默认方法、静态方法和嵌套类型。方法体仅存在于默认方法和静态方法中。接口不能被实例化—它们只能被类实现或其他接口扩展。扩展将在本课程的后面讨论。

定义接口类似于创建新类:

public interface OperateCar {

   // constant declarations, if any

   // method signatures

   // An enum with values RIGHT, LEFT
   int turn(Direction direction,
            double radius,
            double startSpeed,
            double endSpeed);
   int changeLanes(Direction direction,
                   double startSpeed,
                   double endSpeed);
   int signalTurn(Direction direction,
                  boolean signalOn);
   int getRadarFront(double distanceToCar,
                     double speedOfCar);
   int getRadarRear(double distanceToCar,
                    double speedOfCar);
         ......
   // more method signatures
}

请注意,方法签名没有大括号,并以分号结尾。

要使用接口,您需要编写一个实现接口的类。当一个可实例化的类实现一个接口时,它为接口中声明的每个方法提供一个方法体。例如,

public class OperateBMW760i implements OperateCar {

    // the OperateCar method signatures, with implementation --
    // for example:
    public int signalTurn(Direction direction, boolean signalOn) {
       // code to turn BMW's LEFT turn indicator lights on
       // code to turn BMW's LEFT turn indicator lights off
       // code to turn BMW's RIGHT turn indicator lights on
       // code to turn BMW's RIGHT turn indicator lights off
    }

    // other members, as needed -- for example, helper classes not 
    // visible to clients of the interface
}

在上面的机器人汽车示例中,将实现接口的是汽车制造商。雪佛兰的实现肯定与丰田的实现大不相同,但两家制造商都会遵守相同的接口。作为接口的客户,导航制造商将构建使用汽车位置的 GPS 数据、数字街道地图和交通数据来驾驶汽车的系统。在这样做的过程中,导航系统将调用接口方法:转向、变道、刹车、加速等等。

接口作为 API

机器人汽车示例展示了一个作为行业标准应用程序编程接口(API)使用的接口。API 在商业软件产品中也很常见。通常,一家公司销售一个包含复杂方法的软件包,另一家公司希望在自己的软件产品中使用这些方法。一个例子是数字图像处理方法包,这些方法被销售给制作最终用户图形程序的公司。图像处理公司编写其类来实现一个接口,然后将其公开给客户。图形公司然后使用接口中定义的签名和返回类型调用图像处理方法。虽然图像处理公司的 API 是公开的(给其客户),但其 API 的实现被保持为严格保密的秘密—事实上,它可以在以后的某个日期修改实现,只要它继续实现客户依赖的原始接口。

定义一个接口

原文:docs.oracle.com/javase/tutorial/java/IandI/interfaceDef.html

一个接口声明由修饰符、关键字interface、接口名称、一个逗号分隔的父接口列表(如果有)、和接口主体组成。例如:

public interface GroupedInterface extends Interface1, Interface2, Interface3 {

    // constant declarations

    // base of natural logarithms
    double E = 2.718282;

    // method signatures
    void doSomething (int i, double x);
    int doSomethingElse(String s);
}

public访问修饰符表示接口可以被任何包中的任何类使用。如果不指定接口为 public,则接口只能被与接口在同一包中定义的类访问。

一个接口可以扩展其他接口,就像一个类可以子类化或扩展另一个类一样。然而,一个类只能扩展一个其他类,而一个接口可以扩展任意数量的接口。接口声明包括一个逗号分隔的所有它扩展的接口的列表。

接口主体

接口主体可以包含抽象方法,默认方法,和静态方法。接口中的抽象方法后跟一个分号,但不包含大括号(抽象方法不包含实现)。默认方法使用default修饰符定义,静态方法使用static关键字定义。接口中的所有抽象、默认和静态方法都隐式地是public的,因此可以省略public修饰符。

此外,一个接口可以包含常量声明。在接口中定义的所有常量值都隐式地是publicstaticfinal的。再次,你可以省略这些修饰符。

实现一个接口

原文:docs.oracle.com/javase/tutorial/java/IandI/usinginterface.html

要声明一个实现接口的类,你需要在类声明中包含一个implements子句。你的类可以实现多个接口,因此implements关键字后面跟着一个逗号分隔的接口列表。按照惯例,如果有extends子句,则implements子句跟在其后。

一个示例接口,Relatable

考虑一个定义如何比较对象大小的接口。

public interface Relatable {

    // this (object calling isLargerThan)
    // and other must be instances of 
    // the same class returns 1, 0, -1 
    // if this is greater than, 
    // equal to, or less than other
    public int isLargerThan(Relatable other);
}

如果你想要比较相似对象的大小,无论它们是什么,实例化它们的类应该实现Relatable

任何类都可以实现Relatable,只要有一种方法可以比较从该类实例化的对象的相对“大小”。对于字符串,可以是字符数;对于书籍,可以是页数;对于学生,可以是体重;等等。对于平面几何对象,面积是一个不错的选择(参见下面的RectanglePlus类),而对于三维几何对象,体积也可以工作。所有这些类都可以实现isLargerThan()方法。

如果你知道一个类实现了Relatable,那么你就知道可以比较从该类实例化的对象的大小。

实现 Relatable 接口

这里是在创建对象部分中介绍的Rectangle类,重写以实现Relatable

public class RectanglePlus 
    implements Relatable {
    public int width = 0;
    public int height = 0;
    public Point origin;

    // four constructors
    public RectanglePlus() {
        origin = new Point(0, 0);
    }
    public RectanglePlus(Point p) {
        origin = p;
    }
    public RectanglePlus(int w, int h) {
        origin = new Point(0, 0);
        width = w;
        height = h;
    }
    public RectanglePlus(Point p, int w, int h) {
        origin = p;
        width = w;
        height = h;
    }

    // a method for moving the rectangle
    public void move(int x, int y) {
        origin.x = x;
        origin.y = y;
    }

    // a method for computing
    // the area of the rectangle
    public int getArea() {
        return width * height;
    }

    // a method required to implement
    // the Relatable interface
    public int isLargerThan(Relatable other) {
        RectanglePlus otherRect 
            = (RectanglePlus)other;
        if (this.getArea() < otherRect.getArea())
            return -1;
        else if (this.getArea() > otherRect.getArea())
            return 1;
        else
            return 0;               
    }
}

因为RectanglePlus实现了Relatable,所以可以比较任意两个RectanglePlus对象的大小。


注意: 在Relatable接口中定义的isLargerThan方法接受一个Relatable类型的对象。在前面的示例中加粗显示的代码行将other强制转换为RectanglePlus实例。类型转换告诉编译器对象的真实类型。直接在other实例上调用getAreaother.getArea())将无法编译通过,因为编译器不知道other实际上是RectanglePlus的实例。


使用接口作为类型

原文:docs.oracle.com/javase/tutorial/java/IandI/interfaceAsType.html

当你定义一个新接口时,你正在定义一个新的引用数据类型。你可以在任何可以使用其他数据类型名称的地方使用接口名称。如果你定义一个类型为接口的引用变量,那么你分配给它的任何对象必须是实现了该接口的类的实例。

举例来说,这里有一种方法可以找到一对对象中最大的对象,适用于任何从实现了Relatable接口的类实例化的对象:

public Object findLargest(Object object1, Object object2) {
   Relatable obj1 = (Relatable)object1;
   Relatable obj2 = (Relatable)object2;
   if ((obj1).isLargerThan(obj2) > 0)
      return object1;
   else 
      return object2;
}

通过将object1强制转换为Relatable类型,它可以调用isLargerThan方法。

如果你坚持在各种类中实现Relatable,那么从任何这些类实例化的对象都可以使用findLargest()方法进行比较——前提是这两个对象属于同一类。同样,它们也可以使用以下方法进行比较:

public Object findSmallest(Object object1, Object object2) {
   Relatable obj1 = (Relatable)object1;
   Relatable obj2 = (Relatable)object2;
   if ((obj1).isLargerThan(obj2) < 0)
      return object1;
   else 
      return object2;
}

public boolean isEqual(Object object1, Object object2) {
   Relatable obj1 = (Relatable)object1;
   Relatable obj2 = (Relatable)object2;
   if ( (obj1).isLargerThan(obj2) == 0)
      return true;
   else 
      return false;
}

这些方法适用于任何“可比较”的对象,无论它们的类继承关系如何。当它们实现了Relatable接口时,它们可以是自己类(或超类)类型和Relatable类型。这使它们具有多重继承的一些优势,可以同时具有来自超类和接口的行为。

接口的演变

原文:docs.oracle.com/javase/tutorial/java/IandI/nogrow.html

考虑您开发的名为DoIt的接口:

public interface DoIt {
   void doSomething(int i, double x);
   int doSomethingElse(String s);
}

假设以后,您想要向DoIt添加第三个方法,使接口现在变成:

public interface DoIt {

   void doSomething(int i, double x);
   int doSomethingElse(String s);
   boolean didItWork(int i, double x, String s);

}

如果您进行此更改,则所有实现旧DoIt接口的类都将中断,因为它们不再实现旧接口。依赖于此接口的程序员将会强烈抗议。

尽量预见接口的所有用途并从一开始完全指定它。如果要向接口添加其他方法,您有几个选项。您可以创建一个扩展DoItDoItPlus接口:

public interface DoItPlus extends DoIt {

   boolean didItWork(int i, double x, String s);

}

现在,您的代码用户可以选择继续使用旧接口或升级到新接口。

或者,您可以将新方法定义为默认方法。以下示例定义了一个名为didItWork的默认方法:

public interface DoIt {

   void doSomething(int i, double x);
   int doSomethingElse(String s);
   default boolean didItWork(int i, double x, String s) {
       // Method body 
   }

}

请注意,您必须为默认方法提供实现。您还可以为现有接口定义新的静态方法。实现增强了新默认或静态方法的接口的类的用户无需修改或重新编译它们以适应额外的方法。

默认方法

原文:docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

接口部分描述了一个涉及计算机控制汽车制造商发布行业标准接口的示例,描述了可以调用哪些方法来操作他们的汽车。如果这些计算机控制汽车制造商为他们的汽车添加新功能,比如飞行,会怎么样?这些制造商需要指定新的方法来使其他公司(如电子导航仪制造商)能够调整他们的软件以适应飞行汽车。这些汽车制造商会在哪里声明这些新的与飞行相关的方法?如果他们将它们添加到原始接口中,那么已经实现这些接口的程序员将不得不重新编写他们的实现。如果将它们添加为静态方法,那么程序员会将它们视为实用方法,而不是必要的核心方法。

默认方法使您能够向库的接口添加新功能,并确保与为旧版本接口编写的代码的二进制兼容性。

考虑下面的接口,TimeClient,如问题和练习的答案:接口中所述:


import java.time.*; 

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
}

下面的类,SimpleTimeClient,实现了TimeClient


package defaultmethods;

import java.time.*;
import java.lang.*;
import java.util.*;

public class SimpleTimeClient implements TimeClient {

    private LocalDateTime dateAndTime;

    public SimpleTimeClient() {
        dateAndTime = LocalDateTime.now();
    }

    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }

    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }

    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second); 
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }

    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }

    public String toString() {
        return dateAndTime.toString();
    }

    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println(myTimeClient.toString());
    }
}

假设您想要向TimeClient接口添加新功能,比如通过ZonedDateTime对象(类似于LocalDateTime对象,但它存储时区信息)指定时区的能力:

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();                           
    ZonedDateTime getZonedDateTime(String zoneString);
}

TimeClient接口进行这种修改后,您还需要修改SimpleTimeClient类并实现getZonedDateTime方法。但是,与其将getZonedDateTime留空(如前面的例子中),您可以定义一个默认实现。(请记住,抽象方法是声明而没有实现的方法。)


package defaultmethods;

import java.time.*;

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();

    static ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}

您可以在接口中的方法签名开头使用default关键字来指定一个方法定义是默认方法。接口中的所有方法声明,包括默认方法,都隐式地是public的,因此您可以省略public修饰符。

使用这个接口,你不需要修改SimpleTimeClient类,而这个类(以及任何实现TimeClient接口的类)将已经定义好getZonedDateTime方法。下面的例子,TestSimpleTimeClient,调用了SimpleTimeClient实例的getZonedDateTime方法:


package defaultmethods;

import java.time.*;
import java.lang.*;
import java.util.*;

public class TestSimpleTimeClient {
    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println("Current time: " + myTimeClient.toString());
        System.out.println("Time in California: " +
            myTimeClient.getZonedDateTime("Blah blah").toString());
    }
}

扩展包含默认方法的接口

当您扩展包含默认方法的接口时,可以执行以下操作:

  • 完全不提及默认方法,让您扩展的接口继承默认方法。

  • 重新声明默认方法,使其为abstract

  • 重新定义默认方法,覆盖它。

假设您扩展了接口TimeClient如下:

public interface AnotherTimeClient extends TimeClient { }

任何实现接口AnotherTimeClient的类都将具有默认方法TimeClient.getZonedDateTime指定的实现。

假设您扩展了接口TimeClient如下:

public interface AbstractZoneTimeClient extends TimeClient {
    public ZonedDateTime getZonedDateTime(String zoneString);
}

任何实现接口AbstractZoneTimeClient的类都必须实现方法getZonedDateTime;这个方法是一个abstract方法,就像接口中的所有其他非默认(非静态)方法一样。

假设您扩展了接口TimeClient如下:

public interface HandleInvalidTimeZoneClient extends TimeClient {
    default public ZonedDateTime getZonedDateTime(String zoneString) {
        try {
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.of(zoneString)); 
        } catch (DateTimeException e) {
            System.err.println("Invalid zone ID: " + zoneString +
                "; using the default time zone instead.");
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.systemDefault());
        }
    }
}

任何实现接口HandleInvalidTimeZoneClient的类都将使用此接口指定的getZonedDateTime实现,而不是接口TimeClient指定的实现。

静态方法

除了默认方法之外,您还可以在接口中定义静态方法。(静态方法是与定义它的类相关联的方法,而不是与任何对象相关联的方法。类的每个实例共享其静态方法。)这使您更容易在库中组织辅助方法;您可以将特定于接口的静态方法保留在同一接口中,而不是在单独的类中。以下示例定义了一个静态方法,用于检索与时区标识符对应的ZoneId对象;如果没有与给定标识符对应的ZoneId对象,则使用系统默认时区。(因此,您可以简化方法getZonedDateTime):

public interface TimeClient {
    // ...
    static public ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default public ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }    
}

就像类中的静态方法一样,您可以在接口中的方法定义之前使用static关键字指定一个方法是静态方法。接口中的所有方法声明,包括静态方法,都隐式为public,因此您可以省略public修饰符。

将默认方法集成到现有库中

默认方法使您可以向现有接口添加新功能,并确保与为旧版本接口编写的代码具有二进制兼容性。特别是,默认方法使您可以向现有接口添加接受 lambda 表达式作为参数的方法。本节演示了如何通过默认方法和静态方法增强了Comparator接口。

CardDeck类视为问题和练习:类中描述的那样。此示例将CardDeck类重写为接口。Card接口包含两个enum类型(SuitRank)和两个抽象方法(getSuitgetRank):


package defaultmethods;

public interface Card extends Comparable<Card> {

    public enum Suit { 
        DIAMONDS (1, "Diamonds"), 
        CLUBS    (2, "Clubs"   ), 
        HEARTS   (3, "Hearts"  ), 
        SPADES   (4, "Spades"  );

        private final int value;
        private final String text;
        Suit(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }

    public enum Rank { 
        DEUCE  (2 , "Two"  ),
        THREE  (3 , "Three"), 
        FOUR   (4 , "Four" ), 
        FIVE   (5 , "Five" ), 
        SIX    (6 , "Six"  ), 
        SEVEN  (7 , "Seven"),
        EIGHT  (8 , "Eight"), 
        NINE   (9 , "Nine" ), 
        TEN    (10, "Ten"  ), 
        JACK   (11, "Jack" ),
        QUEEN  (12, "Queen"), 
        KING   (13, "King" ),
        ACE    (14, "Ace"  );
        private final int value;
        private final String text;
        Rank(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }

    public Card.Suit getSuit();
    public Card.Rank getRank();
}

Deck接口包含各种操作牌组中卡片的方法:


package defaultmethods; 

import java.util.*;
import java.util.stream.*;
import java.lang.*;

public interface Deck {

    List<Card> getCards();
    Deck deckFactory();
    int size();
    void addCard(Card card);
    void addCards(List<Card> cards);
    void addDeck(Deck deck);
    void shuffle();
    void sort();
    void sort(Comparator<Card> c);
    String deckToString();

    Map<Integer, Deck> deal(int players, int numberOfCards)
        throws IllegalArgumentException;

}

PlayingCard实现了接口Card,而类StandardDeck实现了接口Deck

StandardDeck按如下方式实现了抽象方法Deck.sort

public class StandardDeck implements Deck {

    private List<Card> entireDeck;

    // ...

    public void sort() {
        Collections.sort(entireDeck);
    }

    // ...
}

方法Collections.sort对实现接口Comparable的元素类型为List的实例进行排序。成员entireDeck是一个List的实例,其元素类型为扩展了ComparableCard类型。类PlayingCard按如下方式实现了Comparable.compareTo方法:

public int hashCode() {
    return ((suit.value()-1)*13)+rank.value();
}

public int compareTo(Card o) {
    return this.hashCode() - o.hashCode();
}

方法compareTo导致方法StandardDeck.sort()首先按花色,然后按等级对牌组进行排序。

如果你想先按等级,然后按花色对牌组进行排序怎么办?你需要实现Comparator接口来指定新的排序标准,并使用方法sort(List<T> list, Comparator<? super T> c)(包含Comparator参数的sort方法版本)。你可以在类StandardDeck中定义以下方法:

public void sort(Comparator<Card> c) {
    Collections.sort(entireDeck, c);
}  

有了这个方法,你可以指定方法Collections.sort如何对Card类的实例进行排序。一种方法是实现Comparator接口来指定你希望如何对牌进行排序。示例SortByRankThenSuit就是这样做的:


package defaultmethods;

import java.util.*;
import java.util.stream.*;
import java.lang.*;

public class SortByRankThenSuit implements Comparator<Card> {
    public int compare(Card firstCard, Card secondCard) {
        int compVal =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compVal != 0)
            return compVal;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value(); 
    }
}

以下调用首先按等级,然后按花色对扑克牌组进行排序:

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(new SortByRankThenSuit());

然而,这种方法太啰嗦了;如果你可以只指定排序标准而避免创建多个排序实现,那将更好。假设你是编写Comparator接口的开发人员。你可以向Comparator接口添加哪些默认或静态方法,以使其他开发人员更容易指定排序标准?

首先,假设你想按等级对扑克牌组进行排序,而不考虑花色。你可以如下调用StandardDeck.sort方法:

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) ->
        firstCard.getRank().value() - secondCard.getRank().value()
); 

因为Comparator接口是一个函数式接口,您可以使用 lambda 表达式作为sort方法的参数。在这个例子中,lambda 表达式比较两个整数值。

如果您的开发人员只需调用方法Card.getRank就能创建一个Comparator实例,那将更简单。特别是,如果您的开发人员可以创建一个比较任何可以从getValuehashCode等方法返回数值的对象的Comparator实例,那将很有帮助。Comparator接口已经通过静态方法comparing增强了这种能力:

myDeck.sort(Comparator.comparing((card) -> card.getRank()));  

在这个例子中,您可以使用方法引用:

myDeck.sort(Comparator.comparing(Card::getRank));  

这种调用更好地演示了如何指定不同的排序标准并避免创建多个排序实现。

Comparator接口已经通过其他版本的静态方法comparing(如comparingDoublecomparingLong)进行了增强,使您能够创建比较其他数据类型的Comparator实例。

假设您的开发人员想要创建一个可以根据多个标准比较对象的Comparator实例。例如,如何先按等级,然后按花色对扑克牌进行排序?与以前一样,您可以使用 lambda 表达式来指定这些排序标准:

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) -> {
        int compare =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compare != 0)
            return compare;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value();
    }      
); 

如果您的开发人员可以从一系列Comparator实例构建一个Comparator实例,那将更简单。Comparator接口已经通过默认方法thenComparing增强了这种能力:

myDeck.sort(
    Comparator
        .comparing(Card::getRank)
        .thenComparing(Comparator.comparing(Card::getSuit)));

Comparator接口已经通过其他版本的默认方法thenComparing(如thenComparingDoublethenComparingLong)进行了增强,使您能够构建比较其他数据类型的Comparator实例。

假设您的开发人员想要创建一个Comparator实例,使他们能够以相反的顺序对对象集合进行排序。例如,如何按照牌面从大到小的顺序对扑克牌进行排序,从 A 到 2(而不是从 2 到 A)?与以前一样,您可以指定另一个 lambda 表达式。但是,如果开发人员可以通过调用方法来反转现有的Comparator,那将更简单。Comparator接口已经通过默认方法reversed增强了这种能力:

myDeck.sort(
    Comparator.comparing(Card::getRank)
        .reversed()
        .thenComparing(Comparator.comparing(Card::getSuit)));

这个例子演示了如何通过默认方法、静态方法、lambda 表达式和方法引用增强了Comparator接口,以创建更具表现力的库方法,程序员可以通过查看它们的调用方式快速推断出其功能。使用这些构造来增强您的库中的接口。

接口概要

docs.oracle.com/javase/tutorial/java/IandI/summary-interface.html

接口声明可以包含方法签名、默认方法、静态方法和常量定义。唯一有实现的方法是默认方法和静态方法。

实现接口的类必须实现接口中声明的所有方法。

接口名称可以在任何需要类型的地方使用。

问题和练习:接口

原文:docs.oracle.com/javase/tutorial/java/IandI/QandE/interfaces-questions.html

问题

  1. 一个实现java.lang.CharSequence接口的类需要实现哪些方法?

  2. 以下接口有什么问题?

    public interface SomethingIsWrong {
        void aMethod(int aValue){
            System.out.println("Hi Mom");
        }
    }
    
    
  3. 修复问题 2 中的接口。

  4. 以下接口是否有效?

    public interface Marker {
    }
    
    

练习

  1. 编写一个实现java.lang包中CharSequence接口的类。你的实现应该将字符串倒序返回。从本书中选择一句话作为数据。编写一个小的main方法来测试你的类;确保调用所有四个方法。

  2. 假设你已经编写了一个定期通知其客户端当前日期和时间的时间服务器。编写一个接口,服务器可以使用它来强制执行特定的协议。

检查你的答案。

继承

原文:docs.oracle.com/javase/tutorial/java/IandI/subclasses.html

在前面的课程中,您已经多次看到继承的提及。在 Java 语言中,类可以从其他类派生,从而继承那些类的字段和方法。


定义: 从另一个类派生的类称为子类(也称为派生类扩展类子类)。从子类派生的类称为超类(也称为基类父类)。

除了Object没有超类之外,每个类只有一个直接超类(单继承)。在没有其他显式超类的情况下,每个类都隐式地是Object的子类。

类可以从派生自其他类的类派生,而这些类又从其他类派生,依此类推,最终都是从顶级类Object派生而来。这样的类被称为继承自继承链中一直延伸到Object的所有类。


继承的概念简单而强大:当您想要创建一个新类,并且已经有一个包含您想要的一些代码的类时,您可以从现有类派生您的新类。通过这样做,您可以重用现有类的字段和方法,而无需自己编写(和调试!)它们。

子类从其超类继承所有成员(字段、方法和嵌套类)。构造函数不是成员,因此它们不会被子类继承,但是可以从子类中调用超类的构造函数。

Java 平台类层次结构

Object类,定义在java.lang包中,定义并实现了所有类共有的行为,包括您编写的类。在 Java 平台中,许多类直接从Object派生,其他类从其中一些类派生,依此类推,形成一个类的层次结构。

Java 平台中的所有类都是 Object 的子类

Java 平台中的所有类都是 Object 的子类

在层次结构的顶部,Object是所有类中最通用的类。层次结构底部附近的类提供更专业化的行为。

继承的示例

这是一个可能实现的Bicycle类的示例代码,该代码在类和对象课程中提供:

public class Bicycle {

    // the Bicycle class has three *fields*
    public int cadence;
    public int gear;
    public int speed;

    // the Bicycle class has one *constructor*
    public Bicycle(int startCadence, int startSpeed, int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;
    }

    // the Bicycle class has four *methods*
    public void setCadence(int newValue) {
        cadence = newValue;
    }

    public void setGear(int newValue) {
        gear = newValue;
    }

    public void applyBrake(int decrement) {
        speed -= decrement;
    }

    public void speedUp(int increment) {
        speed += increment;
    }

}

一个MountainBike类的类声明,它是Bicycle的子类,可能如下所示:

public class MountainBike extends Bicycle {

    // the MountainBike subclass adds one *field*
    public int seatHeight;

    // the MountainBike subclass has one *constructor*
    public MountainBike(int startHeight,
                        int startCadence,
                        int startSpeed,
                        int startGear) {
        super(startCadence, startSpeed, startGear);
        seatHeight = startHeight;
    }   

    // the MountainBike subclass adds one *method*
    public void setHeight(int newValue) {
        seatHeight = newValue;
    }   
}

MountainBike继承了Bicycle的所有字段和方法,并添加了字段seatHeight和一个设置它的方法。除了构造函数外,就好像你完全从头开始编写了一个新的MountainBike类,有四个字段和五个方法。但是,你不必做所有的工作。如果Bicycle类中的方法很复杂并且花费了大量时间来调试,这将特别有价值。

在子类中可以做什么

子类继承其父类的所有publicprotected成员,无论子类位于何种包中。如果子类与其父类在同一包中,它还会继承父类的package-private成员。你可以直接使用继承的成员,替换它们,隐藏它们,或者用新成员补充它们:

  • 继承的字段可以直接使用,就像任何其他字段一样。

  • 你可以在子类中声明一个与超类中相同名称的字段,从而隐藏它(不建议)。

  • 你可以在子类中声明超类中没有的新字段。

  • 继承的方法可以直接使用。

  • 你可以在子类中编写一个新的实例方法,其签名与超类中的方法相同,从而覆盖它。

  • 你可以在子类中编写一个新的静态方法,其签名与超类中的方法相同,从而隐藏它。

  • 你可以在子类中声明超类中没有的新方法。

  • 你可以编写一个子类构造函数,隐式地或使用关键字super调用超类的构造函数。

本课程的以下部分将扩展这些主题。

超类中的私有成员

子类不继承其父类的private成员。但是,如果超类有用于访问其私有字段的公共或受保护方法,子类也可以使用这些方法。

嵌套类可以访问其封闭类的所有私有成员—包括字段和方法。因此,一个由子类继承的公共或受保护的嵌套类间接访问了超类的所有私有成员。

对象转型

我们已经看到,一个对象的数据类型是它实例化的类的数据类型。例如,如果我们写

public MountainBike myBike = new MountainBike();

那么myBike的类型是MountainBike

MountainBike是从BicycleObject继承而来的。因此,MountainBike是一个Bicycle,也是一个Object,可以在需要BicycleObject对象的任何地方使用。

反之未必成立:Bicycle可能是MountainBike,但不一定。同样,Object可能是BicycleMountainBike,但不一定。

转型展示了在继承和实现允许的对象之间使用一个类型的对象代替另一个类型的对象。例如,如果我们写

Object obj = new MountainBike();

那么obj既是一个Object,也是一个MountainBike(直到obj被分配为不是MountainBike的另一个对象为止)。这被称为隐式转换

另一方面,如果我们写

MountainBike myBike = obj;

我们会得到一个编译时错误,因为编译器不知道obj是一个MountainBike。然而,我们可以告诉编译器,我们承诺将一个MountainBike分配给obj,通过显式转换

MountainBike myBike = (MountainBike)obj;

这个转换插入了一个运行时检查,以确保obj被分配为MountainBike,这样编译器可以安全地假定obj是一个MountainBike。如果在运行时obj不是MountainBike,则会抛出异常。


注意: 您可以使用instanceof运算符对特定对象的类型进行逻辑测试。这可以避免由于不正确的转换而导致运行时错误。例如:

if (obj instanceof MountainBike) {
    MountainBike myBike = (MountainBike)obj;
}

这里的instanceof运算符验证obj指向一个MountainBike,这样我们可以进行转换,并确保不会抛出运行时异常。


状态、实现和类型的多重继承

原文:docs.oracle.com/javase/tutorial/java/IandI/multipleinheritance.html

类和接口之间的一个重要区别是类可以有字段,而接口不能。此外,您可以实例化一个类来创建一个对象,而接口不能这样做。正如在什么是对象?一节中所解释的,对象将其状态存储在字段中,这些字段在类中定义。Java 编程语言不允许您扩展多个类的一个原因是为了避免状态的多重继承问题,即从多个类继承字段的能力。例如,假设您能够定义一个新类,该类扩展多个类。当您通过实例化该类创建对象时,该对象将从所有超类继承字段。如果不同超类的方法或构造函数实例化相同字段会怎样?哪个方法或构造函数将优先?由于接口不包含字段,您不必担心由于状态的多重继承而导致的问题。

实现的多重继承是从多个类继承方法定义的能力。这种类型的多重继承会引发问题,例如名称冲突和模糊性。当支持这种类型多重继承的编程语言的编译器遇到包含相同名称方法的超类时,有时无法确定要访问或调用哪个成员或方法。此外,程序员可能会通过向超类添加新方法无意中引入名称冲突。默认方法引入了一种实现的多重继承形式。一个类可以实现多个接口,这些接口可以包含具有相同名称的默认方法。Java 编译器提供了一些规则来确定特定类使用哪个默认方法。

Java 编程语言支持类型的多重继承,即一个类可以实现多个接口的能力。一个对象可以有多种类型:它自己类的类型以及类实现的所有接口的类型。这意味着如果一个变量声明为接口的类型,那么它的值可以引用任何实例化自实现该接口的任何类的对象。这在将接口用作类型一节中讨论。

与多重继承实现一样,一个类可以继承在其扩展的接口中定义的方法的不同实现(作为默认或静态)。在这种情况下,编译器或用户必须决定使用哪一个。

覆盖和隐藏方法

原文:docs.oracle.com/javase/tutorial/java/IandI/override.html

实例方法

子类中具有与超类中实例方法相同签名(名称,以及其参数的数量和类型)和返回类型的实例方法覆盖了超类的方法。

子类覆盖方法的能力允许一个类从一个行为“足够接近”的超类继承,然后根据需要修改行为。覆盖方法具有与其覆盖的方法相同的名称、参数数量和类型以及返回类型。覆盖方法还可以返回被覆盖方法返回类型的子类型。这个子类型被称为协变返回类型

在覆盖方法时,您可能希望使用@Override注解,指示编译器您打算覆盖超类中的方法。如果由于某种原因,编译器检测到该方法在任何一个超类中不存在,则会生成一个错误。有关@Override的更多信息,请参见Annotations

静态方法

如果一个子类定义了一个与超类中静态方法相同签名的静态方法,则子类中的方法隐藏超类中的方法。

隐藏静态方法和覆盖实例方法之间的区别具有重要的影响:

  • 调用的覆盖实例方法的版本是在子类中的版本。

  • 调用的隐藏静态方法的版本取决于它是从超类还是从子类调用的。

考虑一个包含两个类的示例。第一个是Animal,包含一个实例方法和一个静态方法:

public class Animal {
    public static void testClassMethod() {
        System.out.println("The static method in Animal");
    }
    public void testInstanceMethod() {
        System.out.println("The instance method in Animal");
    }
}

第二个类,Cat,是Animal的一个子类:

public class Cat extends Animal {
    public static void testClassMethod() {
        System.out.println("The static method in Cat");
    }
    public void testInstanceMethod() {
        System.out.println("The instance method in Cat");
    }

    public static void main(String[] args) {
        Cat myCat = new Cat();
        Animal myAnimal = myCat;
        Animal.testClassMethod();
        myAnimal.testInstanceMethod();
    }
}

Cat类覆盖了Animal中的实例方法,并隐藏了Animal中的静态方法。这个类中的main方法创建了一个Cat的实例,并在类上调用testClassMethod(),在实例上调用testInstanceMethod()

这个程序的输出如下:

The static method in Animal
The instance method in Cat

如约定,调用的隐藏静态方法的版本是在超类中的版本,调用的覆盖实例方法的版本是在子类中的版本。

接口方法

默认方法和抽象方法在接口中像实例方法一样被继承。然而,当一个类或接口的超类型提供了多个具有相同签名的默认方法时,Java 编译器遵循继承规则来解决名称冲突。这些规则受以下两个原则驱动:

  • 实例方法优先于接口默认方法。

    考虑以下类和接口:

    public class Horse {
        public String identifyMyself() {
            return "I am a horse.";
        }
    }
    
    public interface Flyer {
        default public String identifyMyself() {
            return "I am able to fly.";
        }
    }
    
    public interface Mythical {
        default public String identifyMyself() {
            return "I am a mythical creature.";
        }
    }
    
    public class Pegasus extends Horse implements Flyer, Mythical {
        public static void main(String... args) {
            Pegasus myApp = new Pegasus();
            System.out.println(myApp.identifyMyself());
        }
    }
    

    方法Pegasus.identifyMyself返回字符串I am a horse.

  • 已经被其他候选方法覆盖的方法将被忽略。当超类型共享一个共同的祖先时,就会出现这种情况。

    考虑以下接口和类:

    public interface Animal {
        default public String identifyMyself() {
            return "I am an animal.";
        }
    }
    
    public interface EggLayer extends Animal {
        default public String identifyMyself() {
            return "I am able to lay eggs.";
        }
    }
    
    public interface FireBreather extends Animal { }
    
    public class Dragon implements EggLayer, FireBreather {
        public static void main (String... args) {
            Dragon myApp = new Dragon();
            System.out.println(myApp.identifyMyself());
        }
    }
    

    方法Dragon.identifyMyself返回字符串I am able to lay eggs.

如果两个或更多独立定义的默认方法冲突,或者默认方法与抽象方法冲突,则 Java 编译器会产生编译错误。您必须显式覆盖超类型方法。

考虑一下关于现在可以飞行的计算机控制汽车的例子。您有两个接口(OperateCarFlyCar),它们为相同方法(startEngine)提供默认实现:

public interface OperateCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}
public interface FlyCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}

实现OperateCarFlyCar的类必须覆盖方法startEngine。您可以使用super关键字调用任何默认实现中的任何一个。

public class FlyingCar implements OperateCar, FlyCar {
    // ...
    public int startEngine(EncryptedKey key) {
        FlyCar.super.startEngine(key);
        OperateCar.super.startEngine(key);
    }
}

super之前的名称(在本例中为FlyCarOperateCar)必须引用直接定义或继承了被调用方法的超接口。这种形式的方法调用不仅限于区分包含具有相同签名的默认方法的多个实现接口。您可以使用super关键字在类和接口中调用默认方法。

从类中继承的实例方法可以覆盖抽象接口方法。考虑以下接口和类:

public interface Mammal {
    String identifyMyself();
}

public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}

public class Mustang extends Horse implements Mammal {
    public static void main(String... args) {
        Mustang myApp = new Mustang();
        System.out.println(myApp.identifyMyself());
    }
}

方法Mustang.identifyMyself返回字符串I am a horse.Mustang继承自类Horse的方法identifyMyself,该方法覆盖了接口Mammal中同名的抽象方法。

注意:接口中的静态方法不会被继承。

修饰符

覆盖方法的访问修饰符可以允许更多的访问权限,但不能少于被覆盖方法的访问权限。例如,超类中的受保护实例方法可以在子类中变为公共方法,但不能变为私有方法。

如果您尝试将超类中的实例方法更改为子类中的静态方法,或者反之,则会收到编译时错误。

总结

以下表格总结了当您定义一个与超类中方法具有相同签名的方法时会发生什么。

定义一个与超类方法具有相同签名的方法

超类实例方法 超类静态方法
子类实例方法 覆盖 生成编译时错误
子类静态方法 生成编译时错误 隐藏

注意:在子类中,您可以重载从超类继承的方法。这样重载的方法既不隐藏也不覆盖超类实例方法——它们是子类独有的新方法。


多态性

原文:docs.oracle.com/javase/tutorial/java/IandI/polymorphism.html

多态性的词典定义指的是生物学中的一个原则,即一个生物体或物种可以具有许多不同的形式或阶段。这个原则也可以应用于面向对象编程和像 Java 语言这样的语言中。类的子类可以定义自己独特的行为,同时共享父类的一些功能。

多态性可以通过对Bicycle类进行微小修改来演示。例如,可以向类中添加一个printDescription方法,该方法显示实例中当前存储的所有数据。

public void printDescription(){
    System.out.println("\nBike is " + "in gear " + this.gear
        + " with a cadence of " + this.cadence +
        " and travelling at a speed of " + this.speed + ". ");
}

要在 Java 语言中演示多态特性,请使用MountainBikeRoadBike类扩展Bicycle类。对于MountainBike,添加一个suspension字段,这是一个String值,指示自行车是否有前减震器,Front。或者,自行车有前后减震器,Dual

这是更新后的类:

public class MountainBike extends Bicycle {
    private String suspension;

    public MountainBike(
               int startCadence,
               int startSpeed,
               int startGear,
               String suspensionType){
        super(startCadence,
              startSpeed,
              startGear);
        this.setSuspension(suspensionType);
    }

    public String getSuspension(){
      return this.suspension;
    }

    public void setSuspension(String suspensionType) {
        this.suspension = suspensionType;
    }

    public void printDescription() {
        super.printDescription();
        System.out.println("The " + "MountainBike has a" +
            getSuspension() + " suspension.");
    }
} 

请注意覆盖的printDescription方法。除了之前提供的信息外,输出还包括有关悬架的额外数据。

接下来,创建RoadBike类。因为公路或赛车自行车有细轮胎,所以添加一个属性来跟踪轮胎宽度。这是RoadBike类:

public class RoadBike extends Bicycle{
    // In millimeters (mm)
    private int tireWidth;

    public RoadBike(int startCadence,
                    int startSpeed,
                    int startGear,
                    int newTireWidth){
        super(startCadence,
              startSpeed,
              startGear);
        this.setTireWidth(newTireWidth);
    }

    public int getTireWidth(){
      return this.tireWidth;
    }

    public void setTireWidth(int newTireWidth){
        this.tireWidth = newTireWidth;
    }

    public void printDescription(){
        super.printDescription();
        System.out.println("The RoadBike" + " has " + getTireWidth() +
            " MM tires.");
    }
}

请注意,printDescription方法再次被覆盖。这次,显示了有关轮胎宽度的信息。

总结一下,有三个类:BicycleMountainBikeRoadBike。这两个子类覆盖了printDescription方法并打印了独特的信息。

这是一个创建三个Bicycle变量的测试程序。每个变量分配给三个自行车类中的一个。然后打印每个变量。

public class TestBikes {
  public static void main(String[] args){
    Bicycle bike01, bike02, bike03;

    bike01 = new Bicycle(20, 10, 1);
    bike02 = new MountainBike(20, 10, 5, "Dual");
    bike03 = new RoadBike(40, 20, 8, 23);

    bike01.printDescription();
    bike02.printDescription();
    bike03.printDescription();
  }
}

以下是测试程序的输出:

Bike is in gear 1 with a cadence of 20 and travelling at a speed of 10\. 

Bike is in gear 5 with a cadence of 20 and travelling at a speed of 10\. 
The MountainBike has a Dual suspension.

Bike is in gear 8 with a cadence of 40 and travelling at a speed of 20\. 
The RoadBike has 23 MM tires.

Java 虚拟机(JVM)调用与每个变量引用的对象相对应的方法。它不调用变量类型定义的方法。这种行为称为虚拟方法调用,展示了 Java 语言中重要的多态性特性的一个方面。

隐藏字段

原文:docs.oracle.com/javase/tutorial/java/IandI/hidevariables.html

在一个类中,如果一个字段与超类中的字段同名,即使它们的类型不同,该字段也会隐藏超类的字段。在子类中,无法通过简单名称引用超类中的字段。相反,必须通过super来访问该字段,这将在下一节中介绍。一般来说,我们不建议隐藏字段,因为这会使代码难以阅读。

使用关键字 super

原文:docs.oracle.com/javase/tutorial/java/IandI/super.html

访问超类成员

如果您的方法覆盖了其超类的方法之一,您可以通过关键字super调用被覆盖的方法。您还可以使用super来引用隐藏字段(尽管不建议隐藏字段)。考虑这个类,Superclass

public class Superclass {

    public void printMethod() {
        System.out.println("Printed in Superclass.");
    }
}

这里是一个名为Subclass的子类,覆盖了printMethod()

public class Subclass extends Superclass {

    // overrides printMethod in Superclass
    public void printMethod() {
        super.printMethod();
        System.out.println("Printed in Subclass");
    }
    public static void main(String[] args) {
        Subclass s = new Subclass();
        s.printMethod();    
    }
}

Subclass中,简单名称printMethod()指的是在Subclass中声明的那个,它覆盖了Superclass中的那个。因此,要引用从Superclass继承的printMethod()Subclass必须使用一个限定名称,使用super如所示。编译和执行Subclass将打印以下内容:

Printed in Superclass.
Printed in Subclass

子类构造函数

以下示例说明了如何使用super关键字调用超类的构造函数。回想一下Bicycle示例中MountainBikeBicycle的子类。这是MountainBike(子类)构造函数,它调用超类构造函数,然后添加自己的初始化代码:

public MountainBike(int startHeight, 
                    int startCadence,
                    int startSpeed,
                    int startGear) {
    super(startCadence, startSpeed, startGear);
    seatHeight = startHeight;
}   

调用超类构造函数必须是子类构造函数中的第一行。

调用超类构造函数的语法是

super();  

或者:

super(parameter list);

使用super()时,将调用超类的无参数构造函数。使用super(parameter list)时,将调用具有匹配参数列表的超类构造函数。


注意:如果构造函数没有显式调用超类的构造函数,Java 编译器会自动插入对超类的无参数构造函数的调用。如果超类没有无参数构造函数,您将会得到一个编译时错误。Object确实有这样一个构造函数,所以如果Object是唯一的超类,就不会有问题。


如果子类构造函数显式或隐式地调用其超类的构造函数,您可能会认为会有一整个构造函数链被调用,一直回溯到Object的构造函数。事实上,情况确实如此。这被称为构造函数链,当存在长串的类继承时,您需要注意这一点。

作为超类的对象

原文:docs.oracle.com/javase/tutorial/java/IandI/objectclass.html

Object类,位于java.lang包中,位于类层次结构树的顶部。每个类都是Object类的后代,直接或间接的。你使用或编写的每个类都继承了Object的实例方法。你不需要使用这些方法中的任何一个,但是如果选择这样做,可能需要用特定于你的类的代码重写它们。本节讨论的从Object继承的方法有:

  • protected Object clone() throws CloneNotSupportedException

    创建并返回此对象的副本。

  • public boolean equals(Object obj)

    指示某个其他对象是否"等于"这个对象。

  • protected void finalize() throws Throwable

    垃圾回收器在对象上调用的方法

    集合确定没有更多引用指向该对象

  • public final Class getClass()

    返回对象的运行时类。

  • public int hashCode()

    为对象返回一个哈希码值。

  • public String toString()

    返回对象的字符串表示形式。

ObjectnotifynotifyAllwait方法在程序中独立运行的线程的活动同步中起着作用,这将在后面的课程中讨论,这里不会涉及。这些方法有五个:

  • public final void notify()

  • public final void notifyAll()

  • public final void wait()

  • public final void wait(long timeout)

  • public final void wait(long timeout, int nanos)


注意: 这些方法中有一些微妙的方面,特别是clone方法。


clone()方法

如果一个类或其父类实现了Cloneable接口,你可以使用clone()方法从现有对象创建一个副本。要创建一个克隆,你需要编写:

*aCloneableObject*.clone();

Object的这个方法的实现会检查调用clone()的对象是否实现了Cloneable接口。如果对象没有实现,该方法会抛出一个CloneNotSupportedException异常。异常处理将在后面的课程中介绍。目前,你需要知道clone()必须声明为

protected Object clone() throws CloneNotSupportedException

或:

public Object clone() throws CloneNotSupportedException

如果你要编写一个clone()方法来覆盖Object中的方法。

如果调用clone()的对象确实实现了Cloneable接口,Objectclone()方法的实现会创建一个与原始对象相同类的对象,并初始化新对象的成员变量为与原始对象对应的成员变量相同的值。

使你的类可克隆的最简单方法是在类的声明中添加implements Cloneable,然后你的对象可以调用clone()方法。

对于一些类,Objectclone() 方法的默认行为就很好用。然而,如果一个对象包含对外部对象的引用,比如 ObjExternal,你可能需要重写 clone() 来获得正确的行为。否则,一个对象对 ObjExternal 的更改也会在其克隆中可见。这意味着原始对象和其克隆不是独立的—为了解耦它们,你必须重写 clone(),使其克隆对象 ObjExternal。然后原始对象引用 ObjExternal,而克隆引用 ObjExternal 的克隆,这样对象和其克隆就是真正独立的。

equals() 方法

equals() 方法用于比较两个对象是否相等,如果它们相等则返回 trueObject 类中提供的 equals() 方法使用身份运算符 (==) 来确定两个对象是否相等。对于基本数据类型,这会给出正确的结果。然而,对于对象来说,这并不适用。Object 提供的 equals() 方法测试对象的引用是否相等—也就是说,如果比较的对象是完全相同的对象。

要测试两个对象是否在等价性意义上相等(包含相同的信息),你必须重写 equals() 方法。下面是一个重写 equals()Book 类的示例:

public class Book {
    String ISBN;

    public String getISBN() { 
        return ISBN;
    }

    public boolean equals(Object obj) {
        if (obj instanceof Book)
            return ISBN.equals((Book)obj.getISBN()); 
        else
            return false;
    }
}

考虑下面这段代码,用于测试 Book 类的两个实例是否相等:

// Swing Tutorial, 2nd edition
Book firstBook  = new Book("0201914670");
Book secondBook = new Book("0201914670");
if (firstBook.equals(secondBook)) {
    System.out.println("objects are equal");
} else {
    System.out.println("objects are not equal");
}

即使 firstBooksecondBook 引用了两个不同的对象,这个程序会显示 objects are equal。它们被认为是相等的,因为比较的对象包含相同的 ISBN 号码。

如果身份运算符对于你的类不合适,你应该始终重写 equals() 方法。


注意:如果你重写了 equals(),你必须同时重写 hashCode()


finalize() 方法

Object 类提供了一个回调方法 finalize(),当对象变为垃圾时可能会被调用。Objectfinalize() 实现什么也不做—你可以重写 finalize() 来进行清理,比如释放资源。

finalize() 方法可能会被系统自动调用,但是何时调用,甚至是否调用都是不确定的。因此,不要依赖这个方法来清理资源。例如,如果在执行 I/O 操作后没有关闭文件描述符,并且期望 finalize() 为您关闭它们,那么可能会耗尽文件描述符。相反,使用 try-with 资源语句来自动关闭应用程序的资源。参见 The try-with-resources Statement 和 Finalization and Weak, Soft, and Phantom References 中的 Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide

getClass() 方法

你不能重写 getClass

getClass()方法返回一个Class对象,该对象有一些方法可以获取关于类的信息,比如它的名称(getSimpleName())、它的父类(getSuperclass())和它实现的接口(getInterfaces())。例如,下面的方法获取并显示对象的类名:

void printClassName(Object obj) {
    System.out.println("The object's" + " class is " +
        obj.getClass().getSimpleName());
}

Class类,位于java.lang包中,有大量的方法(超过 50 个)。例如,你可以测试类是否为注解(isAnnotation())、接口(isInterface())或枚举(isEnum())。你可以查看对象的字段(getFields())或方法(getMethods()),等等。

hashCode()方法

hashCode()返回的值是对象的哈希码,是由哈希算法生成的整数值。

根据定义,如果两个对象相等,它们的哈希码也必须相等。如果你重写了equals()方法,改变了两个对象相等的方式,那么ObjecthashCode()实现就不再有效。因此,如果你重写了equals()方法,你也必须重写hashCode()方法。

toString()方法

你应该始终考虑在你的类中重写toString()方法。

ObjecttoString()方法返回对象的String表示,对于调试非常有用。对象的String表示完全取决于对象本身,这就是为什么你需要在你的类中重写toString()

使用toString()System.out.println()可以显示对象的文本表示,比如Book的实例:

System.out.println(firstBook.toString());

对于正确重写的toString()方法,会打印出有用的信息,就像这样:

ISBN: 0201914670; The Swing Tutorial; A Guide to Constructing GUIs, 2nd Edition

编写最终类和方法

原文:docs.oracle.com/javase/tutorial/java/IandI/final.html

你可以将类的一些或所有方法声明为最终。在方法声明中使用final关键字表示该方法不能被子类重写。Object类就是这样做的——它的一些方法是final的。

如果一个方法有一个不应该被更改的实现,并且对对象的一致状态至关重要,你可能希望将其设置为最终方法。例如,你可能想要将ChessAlgorithm类中的getFirstPlayer方法设置为最终方法:

class ChessAlgorithm {
    enum ChessPlayer { WHITE, BLACK }
    ...
    final ChessPlayer getFirstPlayer() {
        return ChessPlayer.WHITE;
    }
    ...
}

从构造函数中调用的方法通常应该声明为最终方法。如果构造函数调用一个非最终方法,子类可能重新定义该方法,导致意想不到或不希望的结果。

请注意,你也可以声明整个类为最终类。声明为最终类的类不能被子类化。例如,当创建像String类这样的不可变类时,这是非常有用的。

抽象方法和类

原文:docs.oracle.com/javase/tutorial/java/IandI/abstract.html

抽象类 是一个声明为 abstract 的类—它可能包含或不包含抽象方法。抽象类不能被实例化,但可以被子类化。

抽象方法 是一种声明但没有实现的方法(没有大括号,后面跟着一个分号),如下所示:

abstract void moveTo(double deltaX, double deltaY);

如果一个类包含抽象方法,那么这个类本身必须声明为 abstract,如下所示:

public abstract class GraphicObject {
   // declare fields
   // declare nonabstract methods
   abstract void draw();
}

当一个抽象类被子类化时,子类通常为其父类中的所有抽象方法提供实现。但是,如果没有提供实现,则子类也必须声明为 abstract


注意: 接口 中的方法(参见 接口 部分)如果没有声明为默认或静态,则隐式是抽象的,因此不需要使用 abstract 修饰符。 (可以使用,但是不必要。)


抽象类与接口的比较

抽象类类似于接口。你不能实例化它们,它们可能包含一些声明有或没有实现的方法。然而,使用抽象类,你可以声明非静态和非最终的字段,并定义公共、受保护和私有的具体方法。使用接口,所有字段都自动是公共的、静态的和最终的,你声明或定义的所有方法(作为默认方法)都是公共的。此外,你只能扩展一个类,无论它是否是抽象的,而你可以实现任意数量的接口。

你应该使用抽象类还是接口?

  • 如果你的情况符合以下任何一种情况,请考虑使用抽象类:

    • 你想在几个密切相关的类之间共享代码。

    • 你期望扩展你的抽象类的类有许多共同的方法或字段,或者需要除了 public 之外的访问修饰符(比如 protected 和 private)。

    • 你想声明非静态或非最终字段。这使你能够定义可以访问和修改它们所属对象状态的方法。

  • 如果你的情况符合以下任何一种情况,请考虑使用接口:

    • 你期望不相关的类会实现你的接口。例如,ComparableCloneable 这些接口被许多不相关的类实现。

    • 你想指定特定数据类型的行为,但不关心谁实现它的行为。

    • 你想利用类型的多重继承。

JDK 中抽象类的一个示例是AbstractMap,它是集合框架的一部分。它的子类(包括 HashMapTreeMapConcurrentHashMap)共享许多方法(包括 getputisEmptycontainsKeycontainsValue),这些方法是由 AbstractMap 定义的。

JDK 中实现多个接口的一个类的示例是HashMap,它实现了接口 SerializableCloneableMap<K, V>。通过阅读这些接口列表,你可以推断出 HashMap 的实例(无论是哪个开发者或公司实现的类)可以被克隆,是可序列化的(这意味着它可以被转换为字节流;参见可序列化对象部分),并且具有映射功能。此外,Map<K, V> 接口已经通过许多默认方法(如 mergeforEach)进行了增强,而旧类实现了该接口的类不必定义这些方法。

请注意,许多软件库同时使用抽象类和接口;HashMap 类实现了多个接口,并且还扩展了抽象类 AbstractMap

抽象类示例

在面向对象的绘图应用程序中,你可以绘制圆、矩形、线条、贝塞尔曲线和许多其他图形对象。这些对象都具有某些状态(例如:位置、方向、线条颜色、填充颜色)和行为(例如:moveTo、rotate、resize、draw)是共同的。其中一些状态和行为对所有图形对象都是相同的(例如:位置、填充颜色和 moveTo)。其他需要不同实现(例如,resize 或 draw)。所有 GraphicObject 必须能够自行绘制或调整大小;它们只是在如何执行这些操作上有所不同。这是一个抽象超类的完美情况。你可以利用相似之处,并声明所有图形对象都继承自相同的抽象父对象(例如 GraphicObject),如下图所示。

类 Rectangle、Line、Bezier 和 Circle 继承自 GraphicObject

类 Rectangle、Line、Bezier 和 Circle 继承自 GraphicObject

首先,声明一个抽象类 GraphicObject,以提供所有子类完全共享的成员变量和方法,例如当前位置和 moveTo 方法。GraphicObject 还声明了抽象方法,例如 drawresize,这些方法需要所有子类实现,但必须以不同的方式实现。GraphicObject 类可能如下所示:

abstract class GraphicObject {
    int x, y;
    ...
    void moveTo(int newX, int newY) {
        ...
    }
    abstract void draw();
    abstract void resize();
}

每个非抽象子类 GraphicObject,如 CircleRectangle,必须提供 drawresize 方法的实现:

class Circle extends GraphicObject {
    void draw() {
        ...
    }
    void resize() {
        ...
    }
}
class Rectangle extends GraphicObject {
    void draw() {
        ...
    }
    void resize() {
        ...
    }
}

当抽象类实现接口时

接口部分中指出,实现接口的类必须实现所有接口的方法。然而,可以定义一个不实现所有接口方法的类,只要该类声明为abstract。例如,

abstract class X implements Y {
  // implements all but one method of Y
}

class XX extends X {
  // implements the remaining method in Y
}

在这种情况下,类X必须是abstract,因为它没有完全实现Y,但事实上,类XX实现了Y

类成员

一个抽象类可能有static字段和static方法。您可以像使用其他类一样使用这些静态成员,通过类引用(例如,AbstractClass.staticMethod())。

继承摘要

原文:docs.oracle.com/javase/tutorial/java/IandI/summaryinherit.html

除了Object类外,一个类只有一个直接的父类。一个类从所有直接或间接的父类那里继承字段和方法。子类可以重写继承的方法,或者隐藏继承的字段或方法。(请注意,隐藏字段通常是不良的编程实践。)

在覆盖和隐藏方法部分的表格显示了声明具有与超类中方法相同签名的方法的效果。

Object类是类层次结构的顶部。所有类都是从这个类继承的后代,并从中继承方法。从Object继承的有用方法包括toString()equals()clone()getClass()

通过在类的声明中使用final关键字,可以防止类被子类化。同样,通过将方法声明为最终方法,可以防止子类覆盖它。

抽象类只能被子类化;它不能被实例化。抽象类可以包含抽象方法—声明但未实现的方法。然后子类为抽象方法提供实现。

问题和练习:继承

原文:docs.oracle.com/javase/tutorial/java/IandI/QandE/inherit-questions.html

问题

1. 考虑以下两个类:

public class ClassA {
    public void methodOne(int i) {
    }
    public void methodTwo(int i) {
    }
    public static void methodThree(int i) {
    }
    public static void methodFour(int i) {
    }
}

public class ClassB extends ClassA {
    public static void methodOne(int i) {
    }
    public void methodTwo(int i) {
    }
    public void methodThree(int i) {
    }
    public static void methodFour(int i) {
    }
}

a. 哪个方法覆盖了超类中的方法?

b. 哪个方法隐藏了超类中的方法?

c. 其他方法做什么?

2. 考虑你在问题和练习:类中编写的CardDeckDisplayDeck类。每个类应该覆盖哪些Object方法?

练习

1. 编写你在问题 2 中回答的方法的实现。

检查你的答案。

课程:数字和字符串

原文:docs.oracle.com/javase/tutorial/java/data/index.html

数字

这一部分从讨论Number类(位于java.lang包中)及其子类开始。特别是,本节讨论了在何种情况下您会使用这些类的实例化而不是原始数据类型。此外,本节还讨论了您可能需要与数字一起工作的其他类,例如格式化或使用数学函数来补充语言内置的运算符。最后,还讨论了自动装箱和拆箱,这是一种简化代码的编译器功能。

字符串

字符串在 Java 编程中被广泛使用,它们是字符序列。在 Java 编程语言中,字符串是对象。本节描述了使用String类来创建和操作字符串。它还比较了StringStringBuilder类。

数字

原文:docs.oracle.com/javase/tutorial/java/data/numbers.html

本节开始讨论java.lang包中的Number类,它的子类,以及在何种情况下您会使用这些类的实例化而不是原始数字类型。

本节还介绍了PrintStreamDecimalFormat类,它们提供了用于编写格式化数字输出的方法。

最后,讨论了java.lang中的Math类。它包含了用于补充语言内置运算符的数学函数。该类具有三角函数、指数函数等方法。

数字类

原文:docs.oracle.com/javase/tutorial/java/data/numberclasses.html

在处理数字时,大多数情况下您会在代码中使用原始类型。例如:

int i = 500;
float gpa = 3.65f;
byte mask = 0x7f;

然而,有理由使用对象代替原始类型,并且 Java 平台为每种原始数据类型提供了包装类。这些类将原始类型“包装”在对象中。通常,编译器会执行包装操作—如果您在期望对象的地方使用原始类型,编译器会为您将原始类型装箱到其包装类中。同样,如果您在期望原始类型的地方使用数字对象,编译器会为您拆箱对象。有关更多信息,请参阅自动装箱和拆箱

所有数字包装类都是抽象类Number的子类:

Number 的类层次结构。


注意: 还有四个Number的子类没有在此处讨论。BigDecimalBigInteger用于高精度计算。AtomicIntegerAtomicLong用于多线程应用程序。


有三个原因可能会使用Number对象而不是原始类型:

  1. 作为期望对象的方法的参数(在操作数字集合时经常使用)。

  2. 要使用类定义的常量,如MIN_VALUEMAX_VALUE,提供数据类型的上限和下限。

  3. 要使用类方法将值转换为其他原始类型,将值转换为字符串,以及在不同数字系统之间进行转换(十进制、八进制、十六进制、二进制)。

以下表格列出了所有Number类的子类实现的实例方法。

所有Number子类实现的方法

方法 描述

| `byte byteValue() short shortValue()

int intValue()

long longValue()

float floatValue()

double doubleValue() | 将此Number`对象的值转换为返回的原始数据类型。 |

| `int compareTo(Byte anotherByte) int compareTo(Double anotherDouble)

int compareTo(Float anotherFloat)

int compareTo(Integer anotherInteger)

int compareTo(Long anotherLong)

int compareTo(Short anotherShort) | 将此Number`对象与参数进行比较。 |

| boolean equals(Object obj) | 确定此数字对象是否等于参数。如果参数不为null且为相同类型且具有相同数值的对象,则方法返回true

对于DoubleFloat对象,还有一些额外的要求,这些要求在 Java API 文档中有描述。 |

每个Number类包含其他方法,用于将数字转换为字符串,以及在不同数字系统之间进行转换。以下表格列出了Integer类中的这些方法。其他Number子类的方法类似:

转换方法,Integer

方法 描述
static Integer decode(String s) 将字符串解码为整数。可以接受十进制、八进制或十六进制数字的字符串表示作为输入。
static int parseInt(String s) 返回一个整数(仅限十进制)。
static int parseInt(String s, int radix) 返回一个整数,给定十进制、二进制、八进制或十六进制(radix分别等于 10、2、8 或 16)数字的字符串表示作为输入。
String toString() 返回表示此Integer值的String对象。
static String toString(int i) 返回表示指定整数的String对象。
static Integer valueOf(int i) 返回一个包含指定原始值的Integer对象。
static Integer valueOf(String s) 返回一个Integer对象,其中包含指定字符串表示形式的值。
static Integer valueOf(String s, int radix) 返回一个Integer对象,其中包含指定字符串表示形式的整数值,使用基数进行解析。例如,如果s = "333"radix = 8,则该方法返回八进制数 333 的十进制整数等价值。

格式化数字打印输出

原文:docs.oracle.com/javase/tutorial/java/data/numberformat.html

之前你看到了使用printprintln方法将字符串打印到标准输出(System.out)的示例。由于所有数字都可以转换为字符串(稍后将会看到),你可以使用这些方法打印任意混合的字符串和数字。然而,Java 编程语言还有其他方法,允许你在包含数字时更好地控制打印输出。

printfformat方法

java.io包中包含一个PrintStream类,其中有两个格式化方法可用于替换printprintln。这些方法,formatprintf,是等效的。你一直在使用的熟悉的System.out恰好是一个PrintStream对象,因此你可以在System.out上调用PrintStream方法。因此,在你以前使用printprintln的代码中,你可以使用formatprintf。例如,

System.out.format(.....);

这两个java.io.PrintStream方法的语法是相同的:

public PrintStream format(String format, Object... args)

其中format是一个指定要使用的格式化的字符串,args是要使用该格式化打印的变量列表。一个简单的示例可能是

System.out.format("The value of " + "the float variable is " +
     "%f, while the value of the " + "integer variable is %d, " +
     "and the string is %s", floatVar, intVar, stringVar); 

第一个参数,format,是一个格式字符串,指定了第二个参数args中的对象如何被格式化。格式字符串包含普通文本以及格式说明符,这些是特殊字符,用于格式化Object... args的参数。(Object... args的表示法称为可变参数,意味着参数的数量可能变化。)

格式说明符以百分号(%)开始,并以转换器结束。转换器是指示要格式化的参数类型的字符。在百分号(%)和转换器之间,你可以有可选的标志和说明符。有许多转换器、标志和说明符,这些都在java.util.Formatter中有文档记录。

这里是一个基本示例:

int i = 461012;
System.out.format("The value of i is: %d%n", i);

%d指定单个变量为十进制整数。%n是一个与平台无关的换行符。输出为:

The value of i is: 461012

printfformat方法是重载的。每个都有以下语法版本:

public PrintStream format(Locale l, String format, Object... args)

例如,要在法国系统中打印数字(在英文浮点数表示中使用逗号代替小数点),你可以使用:

System.out.format(Locale.FRANCE,
    "The value of the float " + "variable is %f, while the " +
    "value of the integer variable " + "is %d, and the string is %s%n", 
    floatVar, intVar, stringVar); 

一个示例

下表列出了在接下来的示例程序TestFormat.java中使用的一些转换器和标志。

TestFormat.java中使用的转换器和标志

转换器 标志 说明
d 十进制整数。
f 一个浮点数。
n 适用于运行应用程序的平台的换行符。应始终使用%n,而不是\n
tB 日期和时间转换—月份的区域设置全名。
td, te 日期和时间转换—月份中的 2 位数字。td 根据需要带有前导零,te 则不带。
ty, tY 日期和时间转换—ty = 2 位年份,tY = 4 位年份。
tl 日期和时间转换—12 小时制钟表中的小时。
tM 日期和时间转换—以 2 位数字表示的分钟,必要时带有前导零。
tp 日期和时间转换—区域设置特定的上午/下午(小写)。
tm 日期和时间转换—月份以 2 位数字表示,必要时带有前导零。
tD 日期和时间转换—日期为 %tm%td%ty
08 宽度为 8 个字符,必要时带有前导零。
+ 包括符号,无论是正数还是负数。
, 包括特定于区域设置的分组字符。
- 左对齐。
.3 小数点后三位。
10.3 宽度为 10 个字符,右对齐,小数点后三位。

以下程序展示了您可以使用format进行的一些格式化。输出在嵌入式注释中用双引号显示:

import java.util.Calendar;
import java.util.Locale;

public class TestFormat {

    public static void main(String[] args) {
      long n = 461012;
      System.out.format("%d%n", n);      //  -->  "461012"
      System.out.format("%08d%n", n);    //  -->  "00461012"
      System.out.format("%+8d%n", n);    //  -->  " +461012"
      System.out.format("%,8d%n", n);    // -->  " 461,012"
      System.out.format("%+,8d%n%n", n); //  -->  "+461,012"

      double pi = Math.PI;

      System.out.format("%f%n", pi);       // -->  "3.141593"
      System.out.format("%.3f%n", pi);     // -->  "3.142"
      System.out.format("%10.3f%n", pi);   // -->  "     3.142"
      System.out.format("%-10.3f%n", pi);  // -->  "3.142"
      System.out.format(Locale.FRANCE,
                        "%-10.4f%n%n", pi); // -->  "3,1416"

      Calendar c = Calendar.getInstance();
      System.out.format("%tB %te, %tY%n", c, c, c); // -->  "May 29, 2006"

      System.out.format("%tl:%tM %tp%n", c, c, c);  // -->  "2:34 am"

      System.out.format("%tD%n", c);    // -->  "05/29/06"
    }
}


注意: 本节讨论仅涵盖了formatprintf方法的基础知识。更详细的信息可以在基础教程的基本 I/O部分的“格式化”页面中找到。

使用String.format创建字符串的方法在字符串中有介绍。


DecimalFormat 类

您可以使用java.text.DecimalFormat类来控制前导和尾随零、前缀和后缀、分组(千位)分隔符以及小数点分隔符的显示。DecimalFormat在数字格式化方面提供了很大的灵活性,但可能会使您的代码变得更加复杂。

接下来的示例创建了一个DecimalFormat对象myFormatter,通过将模式字符串传递给DecimalFormat构造函数来创建。然后通过myFormatter调用从NumberFormat继承的format()方法—它接受一个double值作为参数,并以字符串形式返回格式化的数字:

这是一个演示DecimalFormat使用的示例程序:


import java.text.*;

public class DecimalFormatDemo {

   static public void customFormat(String pattern, double value ) {
      DecimalFormat myFormatter = new DecimalFormat(pattern);
      String output = myFormatter.format(value);
      System.out.println(value + "  " + pattern + "  " + output);
   }

   static public void main(String[] args) {

      customFormat("###,###.###", 123456.789);
      customFormat("###.##", 123456.789);
      customFormat("000000.000", 123.78);
      customFormat("$###,###.###", 12345.67);  
   }
}

输出为:

123456.789  ###,###.###  123,456.789
123456.789  ###.##  123456.79
123.78  000000.000  000123.780
12345.67  $###,###.###  $12,345.67

以下表格解释了每行输出。

DecimalFormat.java 输出

模式 输出 解释
123456.789 ###,###.### 123,456.789 井号(#)表示数字,逗号是分组分隔符的占位符,句点是小数分隔符的占位符。
123456.789 ###.## 123456.79 value小数点右侧有三位数字,但pattern只有两位。format方法通过四舍五入处理这个问题。
123.78 000000.000 000123.780 pattern指定了前导和尾随零,因为使用的是 0 字符而不是井号(#)。
12345.67 $###,###.### | $12,345.67 pattern中的第一个字符是美元符号($)。请注意,它紧跟在格式化output中最左边的数字之前。

超出基本算术

原文:docs.oracle.com/javase/tutorial/java/data/beyondmath.html

Java 编程语言支持基本算术运算,使用算术运算符:+,-,*,/和%。java.lang包中的Math类提供了更高级数学计算的方法和常量。

Math类中的方法都是静态的,因此您可以直接从类中调用它们,如下所示:

Math.cos(angle);


注意: 使用static import语言特性,您不必在每个数学函数前面写Math

import static java.lang.Math.*;

这使您可以通过简单名称调用Math类的方法。例如:

cos(angle);


常量和基本方法

Math类包括两个常量:

  • Math.E,即自然对数的底数,以及

  • Math.PI,即圆周与直径的比值。

Math类还包括 40 多个静态方法。以下表格列出了一些基本方法。

基本数学方法

方法 描述

| `double abs(double d) float abs(float f)

int abs(int i)

long abs(long lng) | 返回参数的绝对值。 |

double ceil(double d) 返回大于或等于参数的最小整数。返回为 double 类型。
double floor(double d) 返回小于或等于参数的最大整数。返回为 double 类型。
double rint(double d) 返回与参数最接近的整数值。返回为 double 类型。
long round(double d) int round(float f) 返回最接近参数的 long 或 int,如方法的返回类型所示。

| `double min(double arg1, double arg2) float min(float arg1, float arg2)

int min(int arg1, int arg2)

long min(long arg1, long arg2) | 返回两个参数中较小的值。 |

| `double max(double arg1, double arg2) float max(float arg1, float arg2)

int max(int arg1, int arg2)

long max(long arg1, long arg2) | 返回两个参数中较大的值。 |

以下程序,BasicMathDemo,演示了如何使用其中一些方法:


public class BasicMathDemo {
    public static void main(String[] args) {
        double a = -191.635;
        double b = 43.74;
        int c = 16, d = 45;

        System.out.printf("The absolute value " + "of %.3f is %.3f%n", 
                          a, Math.abs(a));

        System.out.printf("The ceiling of " + "%.2f is %.0f%n", 
                          b, Math.ceil(b));

        System.out.printf("The floor of " + "%.2f is %.0f%n", 
                          b, Math.floor(b));

        System.out.printf("The rint of %.2f " + "is %.0f%n", 
                          b, Math.rint(b));

        System.out.printf("The max of %d and " + "%d is %d%n",
                          c, d, Math.max(c, d));

        System.out.printf("The min of of %d " + "and %d is %d%n",
                          c, d, Math.min(c, d));
    }
}

这个程序的输出如下:

The absolute value of -191.635 is 191.635
The ceiling of 43.74 is 44
The floor of 43.74 is 43
The rint of 43.74 is 44
The max of 16 and 45 is 45
The min of 16 and 45 is 16

指数和对数方法

下表列出了Math类的指数和对数方法。

指数和对数方法

方法 描述
double exp(double d) 返回自然对数的底数 e 的参数次幂。
double log(double d) 返回参数的自然对数。
double pow(double base, double exponent) 返回第一个参数的值的第二个参数的幂。
double sqrt(double d) 返回参数的平方根。

下面的程序,指数演示,显示了e的值,然后对任意选择的数字调用前面表中列出的每个方法:


public class ExponentialDemo {
    public static void main(String[] args) {
        double x = 11.635;
        double y = 2.76;

        System.out.printf("The value of " + "e is %.4f%n",
                          Math.E);

        System.out.printf("exp(%.3f) " + "is %.3f%n",
                          x, Math.exp(x));

        System.out.printf("log(%.3f) is " + "%.3f%n",
                          x, Math.log(x));

        System.out.printf("pow(%.3f, %.3f) " + "is %.3f%n",
                          x, y, Math.pow(x, y));

        System.out.printf("sqrt(%.3f) is " + "%.3f%n",
                          x, Math.sqrt(x));
    }
}

当你运行指数演示时,你将看到以下输出:

The value of e is 2.7183
exp(11.635) is 112983.831
log(11.635) is 2.454
pow(11.635, 2.760) is 874.008
sqrt(11.635) is 3.411

三角函数方法

Math类还提供了一系列三角函数,总结在下表中。传递给这些方法的值是以弧度表示的角度。你可以使用toRadians方法将角度转换为弧度。

三角函数方法

方法 描述
double sin(double d) 返回指定双精度值的正弦值。
double cos(double d) 返回指定双精度值的余弦值。
double tan(double d) 返回指定双精度值的正切值。
double asin(double d) 返回指定双精度值的反正弦值。
double acos(double d) 返回指定双精度值的反余弦值。
double atan(double d) 返回指定双精度值的反正切值。
double atan2(double y, double x) 将直角坐标(x, y)转换为极坐标(r, theta)并返回theta
double toDegrees(double d) double toRadians(double d) 将参数转换为度或弧度。

这里有一个程序,三角函数演示,使用每个方法来计算 45 度角的各种三角函数值:


public class TrigonometricDemo {
    public static void main(String[] args) {
        double degrees = 45.0;
        double radians = Math.toRadians(degrees);

        System.out.format("The value of pi " + "is %.4f%n",
                           Math.PI);

        System.out.format("The sine of %.1f " + "degrees is %.4f%n",
                          degrees, Math.sin(radians));

        System.out.format("The cosine of %.1f " + "degrees is %.4f%n",
                          degrees, Math.cos(radians));

        System.out.format("The tangent of %.1f " + "degrees is %.4f%n",
                          degrees, Math.tan(radians));

        System.out.format("The arcsine of %.4f " + "is %.4f degrees %n", 
                          Math.sin(radians), 
                          Math.toDegrees(Math.asin(Math.sin(radians))));

        System.out.format("The arccosine of %.4f " + "is %.4f degrees %n", 
                          Math.cos(radians),  
                          Math.toDegrees(Math.acos(Math.cos(radians))));

        System.out.format("The arctangent of %.4f " + "is %.4f degrees %n", 
                          Math.tan(radians), 
                          Math.toDegrees(Math.atan(Math.tan(radians))));
    }
}

该程序的输出如下:

The value of pi is 3.1416
The sine of 45.0 degrees is 0.7071
The cosine of 45.0 degrees is 0.7071
The tangent of 45.0 degrees is 1.0000
The arcsine of 0.7071 is 45.0000 degrees
The arccosine of 0.7071 is 45.0000 degrees
The arctangent of 1.0000 is 45.0000 degrees

随机数

random()方法返回一个在 0.0 和 1.0 之间伪随机选择的数字。范围包括 0.0 但不包括 1.0。换句话说:0.0 <= Math.random() < 1.0。要获得不同范围的数字,可以对随机方法返回的值进行算术运算。例如,要生成 0 到 9 之间的整数,你可以这样写:

int number = (int)(Math.random() * 10);

通过将值乘以 10,可能值的范围变为0.0 <= number < 10.0

使用Math.random在需要生成单个随机数时效果很好。如果需要生成一系列随机数,应该创建一个java.util.Random的实例,并在该对象上调用方法来生成数字。

数字总结

原文:docs.oracle.com/javase/tutorial/java/data/numbersummary.html

您可以使用包装类之一 - ByteDoubleFloatIntegerLongShort - 将原始类型的数字包装在对象中。在必要时,Java 编译器会自动为您包装(装箱)原始类型,并在必要时对其进行拆箱。

Number 类包括常量和有用的类方法。MIN_VALUEMAX_VALUE 常量包含该类型对象可以包含的最小值和最大值。byteValueshortValue 和类似方法将一个数值类型转换为另一个数值类型。valueOf 方法将字符串转换为数字,toString 方法将数字转换为字符串。

要格式化包含数字的字符串以进行输出,可以使用 PrintStream 类中的 printf()format() 方法。或者,您可以使用 NumberFormat 类使用模式自定义数字格式。

Math 类包含各种执行数学函数的类方法,包括指数、对数和三角函数方法。Math 还包括基本算术函数,如绝对值和四舍五入,以及用于生成随机数的 random() 方法。

问题和练习:数字

原文:docs.oracle.com/javase/tutorial/java/data/QandE/numbers-questions.html

问题

  1. 使用 API 文档找到以下问题的答案:

    1. 你可以使用哪个Integer方法将int转换为以十六进制表示数字的字符串?例如,哪个方法将整数 65 转换为字符串“41”?

    2. 你会使用哪个Integer方法将以基数 5 表示的字符串转换为相应的int?例如,如何将字符串“230”转换为整数值 65?展示你将用于完成此任务的代码。

    3. 你可以使用哪个 Double 方法来检测浮点数是否具有特殊值“非数字”(NaN)?

  2. 以下表达式的值是多少,为什么?

    Integer.valueOf(1).equals(Long.valueOf(1))
    
    

练习

  1. MaxVariablesDemo更改为显示最小值而不是最大值。您可以删除与变量aCharaBoolean相关的所有代码。输出是什么?

  2. 创建一个程序,从命令行读取未指定数量的整数参数并将它们相加。例如,假设您输入以下内容:

    java Adder 1 3 2 10
    
    

    程序应显示16,然后退出。如果用户只输入一个参数,程序应显示错误消息。您可以基于ValueOfDemo编写您的程序。

  3. 创建一个类似于上一个程序但具有以下区别的程序:

    • 它不是读取整数参数,而是读取浮点参数。

    • 它显示参数的总和,小数点右侧正好有两位。

    例如,假设您输入以下内容:

    java FPAdder 1 1e2 3.0 4.754
    
    

    程序将显示108.75。根据您的区域设置,小数点可能是逗号(,)而不是句号(.)。

检查你的答案。

字符

原文:docs.oracle.com/javase/tutorial/java/data/characters.html

大多数情况下,如果您使用单个字符值,您将使用原始的char类型。例如:

char ch = 'a'; 
// Unicode for uppercase Greek omega character
char uniChar = '\u03A9';
// an array of chars
char[] charArray = { 'a', 'b', 'c', 'd', 'e' };

然而,有时您需要将 char 用作对象—例如,作为期望对象的方法参数。Java 编程语言提供了一个包装器类,用于将char包装在Character对象中。Character类型的对象包含一个单一字段,其类型为char。这个Character类还提供了许多有用的类(即静态)方法来操作字符。

您可以使用Character构造函数创建一个Character对象:

Character ch = new Character('a');

Java 编译器在某些情况下也会为您创建一个Character对象。例如,如果您将一个原始的char传递给一个期望对象的方法,编译器会自动为您将char转换为Character。这个特性被称为自动装箱—或者拆箱,如果转换是另一种方式的话。有关自动装箱和拆箱的更多信息,请参阅自动装箱和拆箱。


注意: Character类是不可变的,一旦创建,Character对象就无法更改。


以下表列出了Character类中一些最有用的方法,但并非详尽无遗。要查看此类中所有方法的完整列表(超过 50 个),请参考java.lang.Character API 规范。

Character类中的有用方法

方法 描述
boolean isLetter(char ch) boolean isDigit(char ch) 确定指定的 char 值是字母还是数字。
boolean isWhitespace(char ch) 确定指定的 char 值是否为空格。
boolean isUpperCase(char ch) boolean isLowerCase(char ch) 确定指定的 char 值是大写还是小写。
char toUpperCase(char ch) char toLowerCase(char ch) 返回指定 char 值的大写或小写形式。
toString(char ch) 返回表示指定字符值的String对象 — 也就是一个字符的字符串。

转义序列

一个由反斜杠(\)引导的字符是一个转义序列,对编译器具有特殊含义。下表显示了 Java 转义序列:

转义序列

转义序列 描述
\t 在文本中插入一个制表符。
\b 在文本中插入一个退格符。
\n 在文本中插入一个换行符。
\r 在文本中插入一个回车符。
\f 在文本中插入一个换页符。
\' 在文本中插入一个单引号字符。
\" 在文本中插入一个双引号字符。
\\ 在文本中插入一个反斜杠字符。

当在打印语句中遇到转义序列时,编译器会相应地解释它。例如,如果你想在引号内放置引号,你必须使用转义序列 \" 来处理内部引号。要打印这个句子

She said "Hello!" to me.

你需要写

System.out.println("She said \"Hello!\" to me.");

字符串

原文:docs.oracle.com/javase/tutorial/java/data/strings.html

在 Java 编程中广泛使用的字符串是一系列字符。在 Java 编程语言中,字符串是对象。

Java 平台提供了String类来创建和操作字符串。

创建字符串

创建字符串的最直接方法是写:

String greeting = "Hello world!";

在这种情况下,Hello world!是一个字符串字面值——代码中用双引号括起来的一系列字符。每当编译器在您的代码中遇到字符串字面值时,它会创建一个带有其值的String对象——在本例中为Hello world!

与任何其他对象一样,您可以使用new关键字和构造函数创建String对象。String类有十三个构造函数,允许您使用不同的来源(如字符数组)提供字符串的初始值:

char[] helloArray = { 'h', 'e', 'l', 'l', 'o', '.' };
String helloString = new String(helloArray);
System.out.println(helloString);

此代码片段的最后一行显示hello


注意:String类是不可变的,因此一旦创建了String对象,就无法更改。String类有许多方法,其中一些将在下面讨论,看起来修改了字符串。由于字符串是不可变的,这些方法实际上是创建并返回一个包含操作结果的新字符串。


字符串长度

用于获取有关对象信息的方法称为访问器方法。您可以与字符串一起使用的一个访问器方法是length()方法,它返回字符串对象中包含的字符数。执行以下两行代码后,len等于 17:

String palindrome = "Dot saw I was Tod";
int len = palindrome.length();

回文是一个对称的单词或句子——它正向和反向拼写相同,忽略大小写和标点符号。这是一个简短且低效的程序来反转一个回文字符串。它调用了String方法charAt(i),该方法返回字符串中第 i 个字符,从 0 开始计数。


public class StringDemo {
    public static void main(String[] args) {
        String palindrome = "Dot saw I was Tod";
        int len = palindrome.length();
        char[] tempCharArray = new char[len];
        char[] charArray = new char[len];

        // put original string in an 
        // array of chars
        for (int i = 0; i < len; i++) {
            tempCharArray[i] = 
                palindrome.charAt(i);
        } 

        // reverse array of chars
        for (int j = 0; j < len; j++) {
            charArray[j] =
                tempCharArray[len - 1 - j];
        }

        String reversePalindrome =
            new String(charArray);
        System.out.println(reversePalindrome);
    }
}

运行程序会产生以下输出:

doT saw I was toD

为了实现字符串反转,程序必须将字符串转换为字符数组(第一个for循环),将数组反转为第二个数组(第二个for循环),然后再转换回字符串。String类包括一个方法,getChars(),用于将字符串或字符串的一部分转换为字符数组,以便我们可以用以下代码替换上面程序中的第一个for循环

palindrome.getChars(0, len, tempCharArray, 0);

字符串连接

String类包括一个用于连接两个字符串的方法:

string1.concat(string2); 

这将返回一个新字符串,其中在字符串 1 的末尾添加了字符串 2。

您还可以使用concat()方法与字符串字面值,如:

"My name is ".concat("Rumplestiltskin");

字符串更常用地使用+运算符连接,如

"Hello," + " world" + "!"

这导致

"Hello, world!"

+运算符广泛用于print语句中。例如:

String string1 = "saw I was ";
System.out.println("Dot " + string1 + "Tod");

打印

Dot saw I was Tod

这样的连接可以是任何对象的混合。对于每个不是String的对象,都会调用它的toString()方法将其转换为String


注意: Java 编程语言不允许文字字符串跨越多行出现在源文件中,因此您必须在多行字符串的每一行末尾使用+连接运算符。例如:

String quote = 
    "Now is the time for all good " +
    "men to come to the aid of their country.";

使用+连接运算符在行之间断开字符串,在print语句中再次非常常见。


创建格式化字符串

您已经看到了使用printf()format()方法打印带有格式化数字的输出。String类有一个等效的类方法format(),它返回一个String对象而不是一个PrintStream对象。

使用String的静态format()方法允许您创建一个格式化的字符串,您可以重复使用,而不是一次性的打印语句。例如,而不是

System.out.printf("The value of the float " +
                  "variable is %f, while " +
                  "the value of the " + 
                  "integer variable is %d, " +
                  "and the string is %s", 
                  floatVar, intVar, stringVar); 

你可以写

String fs;
fs = String.format("The value of the float " +
                   "variable is %f, while " +
                   "the value of the " + 
                   "integer variable is %d, " +
                   " and the string is %s",
                   floatVar, intVar, stringVar);
System.out.println(fs);

在数字和字符串之间进行转换

原文:docs.oracle.com/javase/tutorial/java/data/converting.html

将字符串转换为数字

经常情况下,程序最终会在字符串对象中包含数值数据—例如用户输入的值。

包装原始数值类型的Number子类(ByteIntegerDoubleFloatLongShort)每个都提供一个名为valueOf的类方法,将字符串转换为该类型的对象。以下是一个示例,ValueOfDemo,从命令行获取两个字符串,将它们转换为数字,并对这些值执行算术运算:


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

        // this program requires two 
        // arguments on the command line 
        if (args.length == 2) {
            // convert strings to numbers
            float a = (Float.valueOf(args[0])).floatValue(); 
            float b = (Float.valueOf(args[1])).floatValue();

            // do some arithmetic
            System.out.println("a + b = " +
                               (a + b));
            System.out.println("a - b = " +
                               (a - b));
            System.out.println("a * b = " +
                               (a * b));
            System.out.println("a / b = " +
                               (a / b));
            System.out.println("a % b = " +
                               (a % b));
        } else {
            System.out.println("This program " +
                "requires two command-line arguments.");
        }
    }
}

当您使用4.587.2作为命令行参数时,程序的输出如下:

a + b = 91.7
a - b = -82.7
a * b = 392.4
a / b = 0.0516055
a % b = 4.5


注意:包装原始数值类型的每个Number子类还提供一个parseXXXX()方法(例如,parseFloat()),可用于将字符串转换为原始数值。由于返回的是原始类型而不是对象,因此parseFloat()方法比valueOf()方法更直接。例如,在ValueOfDemo程序中,我们可以使用:

float a = Float.parseFloat(args[0]);
float b = Float.parseFloat(args[1]);


将数字转换为字符串

有时您需要将数字转换为字符串,因为您需要在其字符串形式上进行操作。有几种简单的方法可以将数字转换为字符串:

int i;
// Concatenate "i" with an empty string; conversion is handled for you.
String s1 = "" + i;

或者

// The valueOf class method.
String s2 = String.valueOf(i);

每个Number子类都包括一个类方法toString(),将其原始类型转换为字符串。例如:

int i;
double d;
String s3 = Integer.toString(i); 
String s4 = Double.toString(d); 

ToStringDemo示例使用toString方法将数字转换为字符串。然后程序使用一些字符串方法来计算小数点前后的数字位数:


public class ToStringDemo {

    public static void main(String[] args) {
        double d = 858.48;
        String s = Double.toString(d);

        int dot = s.indexOf('.');

        System.out.println(dot + " digits " +
            "before decimal point.");
        System.out.println( (s.length() - dot - 1) +
            " digits after decimal point.");
    }
}

该程序的输出为:

3 digits before decimal point.
2 digits after decimal point.

操作字符串中的字符

原文:docs.oracle.com/javase/tutorial/java/data/manipstrings.html

String类有许多方法用于检查字符串的内容,在字符串中查找字符或子字符串,更改大小写以及其他任务。

通过索引获取字符和子字符串

您可以通过调用charAt()访问器方法在字符串中获取特定索引处的字符。第一个字符的索引为 0,而最后一个字符的索引为length()-1。例如,以下代码获取字符串中索引为 9 的字符:

String anotherPalindrome = "Niagara. O roar again!"; 
char aChar = anotherPalindrome.charAt(9);

索引从 0 开始,因此索引为 9 的字符是'O',如下图所示:

使用 charAt 方法获取特定索引处的字符。

如果要从字符串中获取多个连续字符,可以使用substring方法。substring方法有两个版本,如下表所示:

String类中的substring方法

方法 描述
String substring(int beginIndex, int endIndex) 返回一个新的字符串,该字符串是此字符串的子字符串。子字符串从指定的beginIndex开始,延伸到索引endIndex - 1的字符。
String substring(int beginIndex) 返回一个新的字符串,该字符串是此字符串的子字符串。整数参数指定第一个字符的索引。在这里,返回的子字符串延伸到原始字符串的末尾。

以下代码从尼亚加拉回文中获取从索引 11 到索引 15 之前的子字符串,即单词"roar":

String anotherPalindrome = "Niagara. O roar again!"; 
String roar = anotherPalindrome.substring(11, 15); 

使用 substring 方法获取字符串的一部分。

用于操作字符串的其他方法

以下是用于操作字符串的几种其他String方法:

String类中用于操作字符串的其他方法

方法 描述
String[] split(String regex) String[] split(String regex, int limit) 搜索由字符串参数指定的匹配项(其中包含正则表达式)并相应地将此字符串拆分为字符串数组。可选的整数参数指定返回数组的最大大小。正则表达式在标题为"正则表达式"的课程中介绍。
CharSequence subSequence(int beginIndex, int endIndex) 返回从beginIndex索引开始直到endIndex - 1的新字符序列。
String trim() 返回删除前导和尾随空格的此字符串的副本。
String toLowerCase() String toUpperCase() 返回转换为小写或大写的此字符串的副本。如果不需要转换,则这些方法返回原始字符串。

在字符串中搜索字符和子字符串

以下是一些用于在字符串中查找字符或子字符串的其他String方法。String类提供了返回特定字符或子字符串在字符串中位置的访问方法:indexOf()lastIndexOf()indexOf()方法从字符串的开头向前搜索,而lastIndexOf()方法从字符串的末尾向后搜索。如果未找到字符或子字符串,indexOf()lastIndexOf()将返回-1。

String类还提供了一个搜索方法contains,如果字符串包含特定的字符序列,则返回 true。当您只需要知道字符串包含一个字符序列,但精确位置并不重要时,请使用此方法。

以下表格描述了各种字符串搜索方法。

String类中的搜索方法

方法 描述
int indexOf(int ch) int lastIndexOf(int ch) 返回指定字符的第一个(最后一个)出现的索引。
int indexOf(int ch, int fromIndex) int lastIndexOf(int ch, int fromIndex) 返回指定字符的第一个(最后一个)出现的索引,从指定索引向前(向后)搜索。
int indexOf(String str) int lastIndexOf(String str) 返回指定子字符串的第一个(最后一个)出现的索引。
int indexOf(String str, int fromIndex) int lastIndexOf(String str, int fromIndex) 返回指定子字符串的第一个(最后一个)出现的索引,从指定索引向前(向后)搜索。
boolean contains(CharSequence s) 如果字符串包含指定的字符序列,则返回 true。

注意: CharSequence是一个由String类实现的接口。因此,您可以将字符串作为contains()方法的参数。


将字符和子字符串替换为字符串

String类几乎没有用于在字符串中插入字符或子字符串的方法。一般情况下,这些方法是不需要的:您可以通过将您从字符串中删除的子字符串与您要插入的子字符串连接起来创建一个新的字符串。

String类确实有四种用于替换找到的字符或子字符串的方法。它们是:

String类中用于操作字符串的方法

方法 描述
String replace(char oldChar, char newChar) 返回一个新字符串,该字符串由将此字符串中所有旧字符的出现替换为新字符而生成。
String replace(CharSequence target, CharSequence replacement) 用指定的替换序列替换此字符串中与字面目标序列匹配的每个子字符串。
String replaceAll(String regex, String replacement) 用给定替换替换此字符串中与给定正则表达式匹配的每个子字符串。
String replaceFirst(String regex, String replacement) 用给定的替换字符串替换与给定正则表达式匹配的此字符串的第一个子字符串。

一个示例

以下类,Filename,演示了使用lastIndexOf()substring()来分离文件名的不同部分。


注意:以下Filename类中的方法没有进行任何错误检查,并假定它们的参数包含完整的目录路径和带有扩展名的文件名。如果这些方法是生产代码,它们将验证它们的参数是否正确构造。



public class Filename {
    private String fullPath;
    private char pathSeparator, 
                 extensionSeparator;

    public Filename(String str, char sep, char ext) {
        fullPath = str;
        pathSeparator = sep;
        extensionSeparator = ext;
    }

    public String extension() {
        int dot = fullPath.lastIndexOf(extensionSeparator);
        return fullPath.substring(dot + 1);
    }

    // gets filename without extension
    public String filename() {
        int dot = fullPath.lastIndexOf(extensionSeparator);
        int sep = fullPath.lastIndexOf(pathSeparator);
        return fullPath.substring(sep + 1, dot);
    }

    public String path() {
        int sep = fullPath.lastIndexOf(pathSeparator);
        return fullPath.substring(0, sep);
    }
}

这里有一个程序,FilenameDemo,它构造了一个Filename对象并调用了它的所有方法:


public class FilenameDemo {
    public static void main(String[] args) {
        final String FPATH = "/home/user/index.html";
        Filename myHomePage = new Filename(FPATH, '/', '.');
        System.out.println("Extension = " + myHomePage.extension());
        System.out.println("Filename = " + myHomePage.filename());
        System.out.println("Path = " + myHomePage.path());
    }
}

以下是程序的输出:

Extension = html
Filename = index
Path = /home/user

如下图所示,我们的extension方法使用lastIndexOf来定位文件名中句点(.)的最后一个出现位置。然后substring使用lastIndexOf的返回值来提取文件名扩展名,即从句点到字符串末尾的子字符串。此代码假定文件名中有一个句点;如果文件名中没有句点,lastIndexOf将返回-1,而substring方法将抛出StringIndexOutOfBoundsException

在文件名类的扩展方法中使用 lastIndexOf 和 substring。

还要注意,extension方法使用dot + 1作为substring的参数。如果句点字符(.)是字符串的最后一个字符,则dot + 1等于字符串的长度,这比字符串的最大索引大 1(因为索引从 0 开始)。这是substring的合法参数,因为该方法接受一个等于但不大于字符串长度的索引,并将其解释为“字符串的结尾”。

比较字符串和字符串部分

原文:docs.oracle.com/javase/tutorial/java/data/comparestrings.html

String类有许多用于比较字符串和字符串部分的方法。以下表格列出了这些方法。

比较字符串的方法

方法 描述
boolean endsWith(String suffix) boolean startsWith(String prefix) 如果此字符串以指定为方法参数的子字符串结尾或以其开头,则返回true
boolean startsWith(String prefix, int offset) 考虑从索引offset开始的字符串,并返回如果以指定为参数的子字符串开头则返回true
int compareTo(String anotherString) 按字典顺序比较两个字符串。返回一个整数,指示此字符串是否大于(结果为> 0)、等于(结果为= 0)或小于(结果为< 0)参数。
int compareToIgnoreCase(String str) 按字典顺序比较两个字符串,忽略大小写差异。返回一个整数,指示此字符串是否大于(结果为> 0)、等于(结果为= 0)或小于(结果为< 0)参数。
boolean equals(Object anObject) 如果参数是表示与此对象相同字符序列的String对象,则返回true
boolean equalsIgnoreCase(String anotherString) 如果参数是表示与此对象相同字符序列的String对象,则返回true,忽略大小写差异。
boolean regionMatches(int toffset, String other, int ooffset, int len) 测试此字符串的指定区域是否与 String 参数的指定区域匹配。区域长度为len,从此字符串的索引toffset和另一个字符串的索引ooffset开始。
boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) 测试此字符串的指定区域是否与 String 参数的指定区域匹配。区域长度为len,从此字符串的索引toffset和另一个字符串的索引ooffset开始。布尔参数指示是否应忽略大小写;如果为 true,则在比较字符时忽略大小写。
boolean matches(String regex) 测试此字符串是否与指定的正则表达式匹配。正则表达式在标题为“正则表达式”的课程中讨论。

以下程序RegionMatchesDemo使用regionMatches方法在另一个字符串中搜索字符串:


public class RegionMatchesDemo {
    public static void main(String[] args) {
        String searchMe = "Green Eggs and Ham";
        String findMe = "Eggs";
        int searchMeLength = searchMe.length();
        int findMeLength = findMe.length();
        boolean foundIt = false;
        for (int i = 0; 
             i <= (searchMeLength - findMeLength);
             i++) {
           if (searchMe.regionMatches(i, findMe, 0, findMeLength)) {
              foundIt = true;
              System.out.println(searchMe.substring(i, i + findMeLength));
              break;
           }
        }
        if (!foundIt)
            System.out.println("No match found.");
    }
}

此程序的输出为Eggs

该程序逐个字符遍历searchMe引用的字符串。对于每个字符,程序调用regionMatches方法来确定从当前字符开始的子字符串是否与程序正在查找的字符串匹配。

StringBuilder 类

原文:docs.oracle.com/javase/tutorial/java/data/buffers.html

StringBuilder 对象类似于 String 对象,不同之处在于它们可以被修改。在内部,这些对象被视为包含一系列字符的可变长度数组。在任何时候,通过方法调用可以更改序列的长度和内容。

除非字符串生成器在代码更简单(请参见本节末尾的示例程序)或性能更好方面提供优势,否则应始终使用字符串。例如,如果需要连接大量字符串,则追加到StringBuilder对象更有效率。

长度和容量

StringBuilder 类,类似于 String 类,具有返回生成器中字符序列长度的 length() 方法。

与字符串不同,每个字符串生成器还有一个容量,即已分配的字符空间数。容量由 capacity() 方法返回,始终大于或等于长度(通常大于),并且会根据需要自动扩展以容纳对字符串生成器的添加。

StringBuilder 构造函数

构造函数 描述
StringBuilder() 创建一个容量为 16(16 个空元素)的空字符串生成器。
StringBuilder(CharSequence cs) 构造一个包含与指定 CharSequence 相同字符的字符串生成器,CharSequence 后面还有额外的 16 个空元素。
StringBuilder(int initCapacity) 创建一个具有指定初始容量的空字符串生成器。
StringBuilder(String s) 创建一个字符串生成器,其值由指定的字符串初始化,字符串后面还有额外的 16 个空元素。

例如,以下代码

// creates empty builder, capacity 16
StringBuilder sb = new StringBuilder();
// adds 9 character string at beginning
sb.append("Greetings");

会生成一个长度为 9,容量为 16 的字符串生成器:

字符串生成器的长度是它包含的字符数;字符串生成器的容量是已分配的字符空间数。

StringBuilder 类有一些与长度和容量相关的方法,而 String 类没有:

长度和容量方法

方法 描述
void setLength(int newLength) 设置字符序列的长度。如果newLength小于length(),则字符序列中的最后字符将被截断。如果newLength大于length(),则在字符序列的末尾添加空字符。
void ensureCapacity(int minCapacity) 确保容量至少等于指定的最小值。

一些操作(例如,append()insert()setLength())可以增加字符串构建器中字符序列的长度,使得结果的length()大于当前的capacity()。当这种情况发生时,容量会自动增加。

StringBuilder 操作

StringBuilder上的主要操作,这些操作在String中不可用,是append()insert()方法,这些方法被重载以接受任何类型的数据。每个方法将其参数转换为字符串,然后将该字符串的字符附加或插入到字符串构建器中的字符序列中。append方法总是在现有字符序列的末尾添加这些字符,而insert方法在指定点添加字符。

这里是StringBuilder类的一些方法。

各种StringBuilder方法

方法 描述

| `StringBuilder append(boolean b) StringBuilder append(char c)

StringBuilder append(char[] str)

StringBuilder append(char[] str, int offset, int len)

StringBuilder append(double d)

StringBuilder append(float f)

StringBuilder append(int i)

StringBuilder append(long lng)

StringBuilder append(Object obj)

StringBuilder append(String s)` | 将参数附加到此字符串构建器。在附加操作发生之前,数据将被转换为字符串。 |

StringBuilder delete(int start, int end) StringBuilder deleteCharAt(int index) 第一个方法删除StringBuilder的字符序列中从startend-1(包括end-1)的子序列。第二个方法删除位于index处的字符。

| `StringBuilder insert(int offset, boolean b) StringBuilder insert(int offset, char c)

StringBuilder insert(int offset, char[] str)

StringBuilder insert(int index, char[] str, int offset, int len)

StringBuilder insert(int offset, double d)

StringBuilder insert(int offset, float f)

StringBuilder insert(int offset, int i)

StringBuilder insert(int offset, long lng)

StringBuilder insert(int offset, Object obj)

StringBuilder insert(int offset, String s)` | 将第二个参数插入到字符串构建器中。第一个整数参数表示要插入数据之前的索引。在插入操作发生之前,数据将被转换为字符串。 |

StringBuilder replace(int start, int end, String s) void setCharAt(int index, char c) 替换此字符串构建器中指定的字符。
StringBuilder reverse() 反转此字符串构建器中的字符序列。
String toString() 返回一个包含构建器中字符序列的字符串。

注意: 您可以通过首先使用StringBuilder类的toString()方法将字符串构建器转换为字符串,然后使用StringBuilder(String str)构造函数将字符串转换回字符串构建器,从而在StringBuilder对象上使用任何String方法。


一个示例

在标题为“字符串”的部分列出的StringDemo程序是一个例子,如果使用StringBuilder而不是String会更有效率。

StringDemo反转了一个回文。这里再次列出它的代码:


public class StringDemo {
    public static void main(String[] args) {
        String palindrome = "Dot saw I was Tod";
        int len = palindrome.length();
        char[] tempCharArray = new char[len];
        char[] charArray = new char[len];

        // put original string in an 
        // array of chars
        for (int i = 0; i < len; i++) {
            tempCharArray[i] = 
                palindrome.charAt(i);
        } 

        // reverse array of chars
        for (int j = 0; j < len; j++) {
            charArray[j] =
                tempCharArray[len - 1 - j];
        }

        String reversePalindrome =
            new String(charArray);
        System.out.println(reversePalindrome);
    }
}

运行程序会产生这个输出:

doT saw I was toD

为了实现字符串反转,程序将字符串转换为字符数组(第一个for循环),将数组反转为第二个数组(第二个for循环),然后再转换回字符串。

如果你将palindrome字符串转换为一个字符串生成器,你可以使用StringBuilder类中的reverse()方法。这样代码会更简单,更易于阅读:


public class StringBuilderDemo {
    public static void main(String[] args) {
        String palindrome = "Dot saw I was Tod";

        StringBuilder sb = new StringBuilder(palindrome);

        sb.reverse();  // reverse it

        System.out.println(sb);
    }
}

运行这个程序会产生相同的输出:

doT saw I was toD

注意,println()打印一个字符串生成器,如下所示:

System.out.println(sb);

因为sb.toString()会被隐式调用,就像在println()调用中对任何其他对象一样。


注意: 还有一个StringBuffer类,与StringBuilder完全相同,唯一的区别是它的方法是同步的,因此是线程安全的。线程将在并发课程中讨论。


字符和字符串总结

原文:docs.oracle.com/javase/tutorial/java/data/stringsummary.html

大多数情况下,如果您使用单个字符值,您将使用基本的char类型。然而,有时您需要将 char 用作对象—例如,作为期望对象的方法参数。Java 编程语言为此提供了一个包装类,将char包装在Character对象中。Character类型的对象包含一个类型为char的单个字段。这个Character类还提供了许多有用的类(即静态)方法来操作字符。

字符串是字符序列,在 Java 编程中被广泛使用。在 Java 编程语言中,字符串是对象。String类有 60 多个方法和 13 个构造函数。

最常见的是,您可以使用类似于以下语句创建一个字符串

String s = "Hello world!";

而不是使用其中一个String构造函数。

String类有许多方法可以查找和检索子字符串;然后可以使用+连接运算符将它们轻松重新组装成新的字符串。

String类还包括许多实用方法,其中包括split()toLowerCase()toUpperCase()valueOf()。后者方法在将用户输入的字符串转换为数字时是不可或缺的。Number子类还有将字符串转换为数字以及反之的方法。

除了String类之外,还有一个StringBuilder类。与字符串一起工作相比,使用StringBuilder对象有时可能更有效率。StringBuilder类提供了一些对字符串有用的方法,其中包括reverse()。然而,总的来说,String类具有更广泛的方法。

可以使用StringBuilder构造函数将字符串转换为字符串构建器。可以使用toString()方法将字符串构建器转换为字符串。

自动装箱和拆箱

原文:docs.oracle.com/javase/tutorial/java/data/autoboxing.html

自动装箱是 Java 编译器在原始类型和其对应的对象包装类之间进行的自动转换。例如,将int转换为Integer,将double转换为Double等。如果转换反向进行,则称为拆箱

这是自动装箱的最简单示例:

Character ch = 'a';

本节中的其余示例使用泛型。如果您还不熟悉泛型的语法,请参阅泛型(更新)课程。

考虑以下代码:

List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
    li.add(i);

尽管您将int值作为原始类型而不是Integer对象添加到li中,但代码仍然可以编译。因为liInteger对象的列表,而不是int值的列表,您可能会想知道为什么 Java 编译器没有发出编译时错误。编译器不会生成错误,因为它从i创建一个Integer对象并将该对象添加到li中。因此,编译器在运行时将前面的代码转换为以下代码:

List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
    li.add(Integer.valueOf(i));

将原始值(例如int)转换为相应包装类(Integer)的对象称为自动装箱。当原始值是以下情况时,Java 编译器会应用自动装箱:

  • 作为传递给期望相应包装类对象的方法的参数。

  • 赋给相应包装类的变量。

考虑以下方法:

public static int sumEven(List<Integer> li) {
    int sum = 0;
    for (Integer i: li)
        if (i % 2 == 0)
            sum += i;
        return sum;
}

因为余数(%)和一元加号(+=)运算符不适用于Integer对象,您可能会想知道为什么 Java 编译器在不发出任何错误的情况下编译该方法。编译器不会生成错误,因为它在运行时调用intValue方法将Integer转换为int

public static int sumEven(List<Integer> li) {
    int sum = 0;
    for (Integer i : li)
        if (i.intValue() % 2 == 0)
            sum += i.intValue();
        return sum;
}

将包装类型的对象(Integer)转换为其对应的原始类型(int)值称为拆箱。当包装类的对象是以下情况时,Java 编译器会应用拆箱:

  • 作为传递给期望相应原始类型值的方法的参数。

  • 赋给相应原始类型的变量。

Unboxing示例展示了这是如何工作的:

import java.util.ArrayList;
import java.util.List;

public class Unboxing {

    public static void main(String[] args) {
        Integer i = new Integer(-8);

        // 1\. Unboxing through method invocation
        int absVal = absoluteValue(i);
        System.out.println("absolute value of " + i + " = " + absVal);

        List<Double> ld = new ArrayList<>();
        ld.add(3.1416);    // Π is autoboxed through method invocation.

        // 2\. Unboxing through assignment
        double pi = ld.get(0);
        System.out.println("pi = " + pi);
    }

    public static int absoluteValue(int i) {
        return (i < 0) ? -i : i;
    }
}

该程序打印如下内容:

absolute value of -8 = 8
pi = 3.1416

自动装箱和拆箱使开发人员编写更清晰的代码,使其更易于阅读。以下表列出了原始类型及其对应的包装类,这些包装类由 Java 编译器用于自动装箱和拆箱:

Primitive type Wrapper class
boolean Boolean
byte Byte
char Character
float Float
int Integer
long Long
short Short
double Double

问题和练习:字符和字符串

原文:docs.oracle.com/javase/tutorial/java/data/QandE/characters-questions.html

问题

  1. 以下字符串构建器的初始容量是多少?

    StringBuilder sb = new StringBuilder("Able was I ere I saw Elba.");
    
    
  2. 考虑以下字符串:

    String hannah = "Did Hannah see bees? Hannah did.";
    
    
    1. 表达式hannah.length()显示的值是多少?

    2. 方法调用hannah.charAt(12)返回的值是多少?

    3. 编写一个表达式,引用hannah所指的字符串中的字母b

  3. 以下表达式返回的字符串有多长?这个字符串是什么?

    "Was it a car or a cat I saw?".substring(9, 12)
    
    
  4. 在下面的程序中,名为ComputeResult,每个编号行执行后result的值是多少?

    public class ComputeResult {
        public static void main(String[] args) {
            String original = "software";
            StringBuilder result = new StringBuilder("hi");
            int index = original.indexOf('a');
    
    /*1*/   result.setCharAt(0, original.charAt(0));
    /*2*/   result.setCharAt(1, original.charAt(original.length()-1));
    /*3*/   result.insert(1, original.charAt(4));
    /*4*/   result.append(original.substring(1,4));
    /*5*/   result.insert(3, (original.substring(index, index+2) + " ")); 
    
            System.out.println(result);
        }
    }
    
    

练习

  1. 展示两种方法将以下两个字符串连接在一起以得到字符串"Hi, mom."

    String hi = "Hi, ";
    String mom = "mom.";
    
    
  2. 编写一个程序,从你的全名中计算出你的缩写并显示出来。

  3. 一个变位词是由另一个单词或短语的字母重新排列而成的单词或短语;例如,“parliament”是“partial men”的变位词,“software”是“swear oft”的变位词。编写一个程序,判断一个字符串是否是另一个字符串的变位词。该程序应忽略空格和标点符号。

检查你的答案。

Lesson: 泛型(更新)

原文:docs.oracle.com/javase/tutorial/java/generics/index.html

在任何非平凡的软件项目中,错误都是生活中不可避免的事实。仔细的规划、编程和测试可以帮助减少它们的普遍性,但不知何故,它们总会找到一种方式悄悄地潜入你的代码中。随着新功能的引入和代码库规模与复杂性的增长,这一点变得尤为明显。

幸运的是,有些错误比其他错误更容易检测。例如,编译时错误可以在早期被检测出来;你可以利用编译器的错误消息来找出问题所在并立即修复它。然而,运行时错误可能会更加棘手;它们并不总是立即显现,而且当它们出现时,可能是在程序中与问题实际原因相距甚远的地方。

泛型通过在编译时使更多的错误可检测,为你的代码增加了稳定性。完成本课程后,你可能想继续学习 Gilad Bracha 的《泛型》教程。

为什么使用泛型?

原文:docs.oracle.com/javase/tutorial/java/generics/why.html

简而言之,泛型使类型(类和接口)在定义类、接口和方法时成为参数。就像在方法声明中使用的更熟悉的形式参数一样,类型参数提供了一种方式让您可以重复使用相同的代码以不同的输入。不同之处在于,形式参数的输入是值,而类型参数的输入是类型。

使用泛型的代码比不使用泛型的代码有许多好处:

  • 编译时进行更强的类型检查。

    Java 编译器对泛型代码应用强类型检查,如果代码违反类型安全性,则会发出错误。修复编译时错误比修复运行时错误更容易,后者可能很难找到。

  • 消除强制类型转换。

    不使用泛型的以下代码片段需要进行强制类型转换:

    List list = new ArrayList();
    list.add("hello");
    String s = (String) list.get(0);
    
    

    当重写为使用泛型时,代码不需要进行强制类型转换:

    List<String> list = new ArrayList<String>();
    list.add("hello");
    String s = list.get(0);   // no cast
    
    
  • 使程序员能够实现泛型算法。

    通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,可以进行定制,并且是类型安全且更易阅读的。

泛型类型

原文:docs.oracle.com/javase/tutorial/java/generics/types.html

泛型类型是一个参数化类型的泛型类或接口。下面的 Box 类将被修改以演示这个概念。

一个简单的 Box 类

首先看一下一个操作任意类型对象的非泛型 Box 类。它只需要提供两个方法:set,用于向盒子中添加对象,和 get,用于检索对象:

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

由于它的方法接受或返回一个 Object,你可以自由传入任何你想要的东西,只要不是原始类型之一。在编译时无法验证类的使用方式。代码的一部分可能将一个 Integer 放入盒子中,并期望从中获取 Integer,而代码的另一部分可能错误地传入一个 String,导致运行时错误。

Box 类的泛型版本

一个泛型类的定义格式如下:

class name<T1, T2, ..., Tn> { /* ... */ }

类型参数部分,由尖括号(<>)界定,跟在类名后面。它指定了类型参数(也称为类型变量T1T2、... 和 Tn

要将 Box 类更新为使用泛型,你需要通过将代码 "public class Box" 更改为 "public class Box<T>" 来创建一个泛型类型声明。这引入了类型变量 T,可以在类内部的任何地方使用。

有了这个改变,Box 类变成了:

/**
 * Generic version of the Box class.
 * @param <T> the type of the value being boxed
 */
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

正如你所看到的,所有 Object 的出现都被 T 替换。类型变量可以是你指定的任何非原始类型:任何类类型、任何接口类型、任何数组类型,甚至是另一个类型变量。

这种技术也可以应用于创建泛型接口。

类型参数命名约定

按照惯例,类型参数的名称是单个大写字母。这与你已经了解的变量命名约定形成鲜明对比,而且有充分的理由:没有这个约定,很难区分类型变量和普通类或接口名称。

最常用的类型参数名称有:

  • E - Element(Java 集合框架广泛使用)

  • K - Key

  • N - Number

  • T - Type

  • V - Value

  • S,U,V 等 - 第二、第三、第四种类型

你将在 Java SE API 和本课程的其余部分中看到这些名称的使用。

调用和实例化泛型类型

要在代码中引用泛型 Box 类,你必须执行一个泛型类型调用,将 T 替换为某个具体值,比如 Integer

Box<Integer> integerBox;

你可以将泛型类型调用看作类似于普通方法调用,但是不是向方法传递参数,而是向 Box 类本身传递一个类型参数 — 在本例中是 Integer


类型参数和类型参数术语: 许多开发人员将“类型参数”和“类型参数”这两个术语互换使用,但这两个术语并不相同。在编码时,为了创建参数化类型,需要提供类型参数。因此,在Foo<T>中,T是类型参数,而在Foo<String> f中的String是类型参数。本课程在使用这些术语时遵守此定义。


与任何其他变量声明一样,此代码实际上并不创建新的Box对象。它只是声明integerBox将保存对“Box of Integer”的引用,这就是Box<Integer>的含义。

通常将泛型类型的调用称为参数化类型

要实例化此类,像往常一样使用new关键字,但是在类名和括号之间放置<Integer>

Box<Integer> integerBox = new Box<Integer>();

钻石

在 Java SE 7 及更高版本中,只要编译器可以从上下文中确定或推断出类型参数,就可以用空类型参数集(<>)替换调用泛型类构造函数所需的类型参数。这一对尖括号<>非正式地称为钻石。例如,您可以使用以下语句创建Box<Integer>的实例:

Box<Integer> integerBox = new Box<>();

有关钻石符号和类型推断的更多信息,请参见类型推断。

多个类型参数

如前所述,泛型类可以具有多个类型参数。例如,实现泛型Pair接口的泛型OrderedPair类:

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}

以下语句创建了OrderedPair类的两个实例:

Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String>  p2 = new OrderedPair<String, String>("hello", "world");

代码new OrderedPair<String, Integer>实例化KStringVInteger。因此,OrderedPair的构造函数的参数类型分别为StringInteger。由于自动装箱,将Stringint传递给类是有效的。

如钻石中所述,因为 Java 编译器可以从声明OrderedPair<String, Integer>中推断出KV类型,所以可以使用钻石符号缩短这些语句:

OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String>  p2 = new OrderedPair<>("hello", "world");

要创建泛型接口,遵循与创建泛型类相同的约定。

参数化类型

您还可以用参数化类型(即List<String>)替换类型参数(即KV)。例如,使用OrderedPair<K, V>示例:

OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));

原始类型

原文:docs.oracle.com/javase/tutorial/java/generics/rawTypes.html

原始类型是没有任何类型参数的泛型类或接口的名称。例如,给定泛型Box类:

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}

要创建Box<T>的参数化类型,您需要为形式类型参数T提供实际类型参数:

Box<Integer> intBox = new Box<>();

如果省略实际类型参数,则创建Box<T>的原始类型:

Box rawBox = new Box();

因此,Box是泛型类型Box<T>的原始类型。但是,非泛型类或接口类型是原始类型。

在旧代码中会出现原始类型,因为在 JDK 5.0 之前,许多 API 类(如Collections类)都不是泛型的。使用原始类型时,实际上获得的是泛型之前的行为 —— Box会给您Object。为了向后兼容,允许将参数化类型分配给其原始类型:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK

但是,如果将原始类型赋给参数化类型,则会收到警告:

Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

如果使用原始类型调用相应泛型类型中定义的泛型方法,也会收到警告:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

此警告显示原始类型绕过了泛型类型检查,将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型。

类型擦除部分提供了有关 Java 编译器如何使用原始类型的更多信息。

未经检查的错误消息

如前所述,在将旧代码与泛型代码混合使用时,可能会遇到类似以下的警告消息:

Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

当使用在原始类型上操作的旧 API 时,可能会出现以下示例中所示的情况:

public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();
    }

    static Box createBox(){
        return new Box();
    }
}

“未经检查”一词表示编译器没有足够的类型信息来执行确保类型安全所需的所有类型检查。默认情况下,“未经检查”警告是禁用的,尽管编译器会给出提示。要查看所有“未经检查”警告,请使用-Xlint:unchecked重新编译。

使用-Xlint:unchecked重新编译前面的示例,会显示以下额外信息:

WarningDemo.java:4: warning: [unchecked] unchecked conversion
found   : Box
required: Box<java.lang.Integer>
        bi = createBox();
                      ^
1 warning

要完全禁用未经检查的警告,请使用-Xlint:-unchecked标志。@SuppressWarnings("unchecked")注解可以抑制未经检查的警告。如果您不熟悉@SuppressWarnings语法,请参阅注解。

泛型方法

原文:docs.oracle.com/javase/tutorial/java/generics/methods.html

泛型方法 是引入自己类型参数的方法。这类似于声明一个泛型类型,但类型参数的范围仅限于声明它的方法。允许静态和非静态泛型方法,以及泛型类构造方法。

泛型方法的语法包括一个类型参数列表,在方法返回类型之前出现在尖括号内。对于静态泛型方法,类型参数部分必须出现在方法返回类型之前。

Util 类包含一个泛型方法 compare,用于比较两个 Pair 对象:

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

调用这个方法的完整语法如下:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);

类型已经明确提供,如粗体所示。通常情况下,这部分可以省略,编译器会推断所需的类型:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);

这个特性被称为类型推断,允许您将泛型方法作为普通方法调用,而无需在尖括号之间指定类型。这个主题在下一节 类型推断 中进一步讨论。

有界类型参数

原文:docs.oracle.com/javase/tutorial/java/generics/bounded.html

有时候你可能想要限制可以用作参数化类型中类型参数的类型。例如,一个操作数字的方法可能只想接受Number或其子类的实例。这就是有界类型参数的用途。

要声明一个有界类型参数,列出类型参数的名称,后跟extends关键字,后跟其上界,在这个例子中是Number。请注意,在这个上下文中,extends的意思是"扩展"(如类)或"实现"(如接口)。

public class Box<T> {

    private T t;          

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // error: this is still String!
    }
}

通过修改我们的通用方法以包含这个有界类型参数,编译现在会失败,因为我们对inspect的调用仍然包括一个String

Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
  be applied to (java.lang.String)
                        integerBox.inspect("10");
                                  ^
1 error

除了限制你可以用来实例化泛型类型的类型之外,有界类型参数还允许你调用边界中定义的方法:

public class NaturalNumber<T extends Integer> {

    private T n;

    public NaturalNumber(T n)  { this.n = n; }

    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }

    // ...
}

isEven方法通过n调用了Integer类中定义的intValue方法。

多个边界

前面的例子说明了使用具有单个边界的类型参数,但是类型参数可以有多个边界

<T extends B1 & B2 & B3>

具有多个边界的类型变量是边界中列出的所有类型的子类型。如果边界中有一个类,它必须首先指定。例如:

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }

如果边界A没有首先指定,你会得到一个编译时错误:

class D <T extends B & A & C> { /* ... */ }  // compile-time error

泛型方法和有界类型参数

原文:docs.oracle.com/javase/tutorial/java/generics/boundedTypeParams.html

有界类型参数是实现泛型算法的关键。考虑以下方法,该方法计算数组T[]中大于指定元素elem的元素数量。

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

该方法的实现很简单,但它无法编译,因为大于运算符(>)仅适用于原始类型,如shortintdoublelongfloatbytechar。你不能使用>运算符来比较对象。为了解决这个问题,使用一个由Comparable<T>接口限定的类型参数:

public interface Comparable<T> {
    public int compareTo(T o);
}

最终的代码将是:

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

泛型、继承和子类型

译文:docs.oracle.com/javase/tutorial/java/generics/inheritance.html

正如你已经知道的,可以将一个类型的对象赋给另一个类型的对象,前提是这两种类型是兼容的。例如,你可以将一个 Integer 赋给一个 Object,因为 ObjectInteger 的超类型之一:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

在面向对象的术语中,这被称为“是一个”关系。由于 Integer Object 的一种,所以赋值是允许的。但是 Integer 也是 Number 的一种,所以下面的代码也是有效的:

public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

泛型也是如此。你可以执行泛型类型调用,将 Number 作为其类型参数,并且如果参数与 Number 兼容,则允许任何后续的 add 调用:

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

现在考虑以下方法:

public void boxTest(Box<Number> n) { /* ... */ }

它接受什么类型的参数?通过查看其签名,你可以看到它接受一个类型为 Box<Number> 的单个参数。但这意味着什么?你可以传入 Box<Integer>Box<Double> 吗,正如你可能期望的那样?答案是“不可以”,因为 Box<Integer>Box<Double> 不是 Box<Number> 的子类型。

这是在使用泛型进行编程时的一个常见误解,但这是一个重要的概念需要学习。

显示 Box 不是 Box 的子类型的图表Box<Integer> 不是 Box<Number> 的子类型,即使 IntegerNumber 的子类型。


注意:给定两个具体类型 AB(例如,NumberInteger),MyClass<A>MyClass<B> 没有关系,无论 AB 是否相关。MyClass<A>MyClass<B> 的共同父类是 Object

有关如何在类型参数相关的情况下创建两个泛型类之间类似子类型的关系的信息,请参阅通配符和子类型。


通用类和子类型

你可以通过扩展或实现来对泛型类或接口进行子类型化。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extendsimplements 子句确定。

Collections 类为例,ArrayList<E> 实现了 List<E>,而 List<E> 扩展了 Collection<E>。因此,ArrayList<String>List<String> 的子类型,List<String>Collection<String> 的子类型。只要不改变类型参数,类型之间的子类型关系就会保持不变。

显示一个示例集合层次结构的图表:ArrayList 是 List 的子类型,List 是 Collection 的子类型。一个示例 Collections 层次结构

现在想象一下,我们想要定义自己的列表接口,PayloadList,它将泛型类型P的可选值与每个元素关联起来。它的声明可能如下所示:

interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}

以下对PayloadList的参数化是List<String>的子类型:

  • PayloadList<String,String>

  • PayloadList<String,Integer>

  • PayloadList<String,Exception>

显示一个示例 PayLoadList 层次结构的图表:PayloadList<String, String>是 List的子类型,List是 Collection的子类型。在 PayloadList<String,String>的同一级别是 PayloadList<String, Integer>和 PayloadList<String, Exceptions>。一个PayloadList层次结构示例

类型推断

原文:docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html

类型推断是 Java 编译器根据每个方法调用和相应声明来确定使调用适用的类型参数(或参数)的能力。推断算法确定参数的类型,以及如果可用的话,结果被分配或返回的类型。最后,推断算法尝试找到适用于所有参数的最具体类型。

为了说明最后一点,在以下示例中,推断确定传递给pick方法的第二个参数的类型为Serializable

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

类型推断和泛型方法

泛型方法向您介绍了类型推断,使您能够调用泛型方法,就像调用普通方法一样,而无需在尖括号之间指定类型。考虑以下示例,BoxDemo,它需要Box类:

public class BoxDemo {

  public static <U> void addBox(U u, 
      java.util.List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }

  public static void main(String[] args) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
    BoxDemo.outputBoxes(listOfIntegerBoxes);
  }
}

以下是此示例的输出:

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

泛型方法addBox定义了一个名为U的类型参数。通常,Java 编译器可以推断泛型方法调用的类型参数。因此,在大多数情况下,您不必指定它们。例如,要调用泛型方法addBox,您可以使用类型见证指定类型参数如下:

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

另外,如果省略类型见证,Java 编译器会自动推断(从方法的参数中)类型参数为Integer

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

类型推断和泛型类的实例化

只要编译器可以从上下文中推断出类型参数,您可以用一组空类型参数(<>)替换调用泛型类构造函数所需的类型参数。这一对尖括号非正式地称为菱形。

例如,考虑以下变量声明:

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

您可以用一组空类型参数(<>)替换构造函数的参数化类型:

Map<String, List<String>> myMap = new HashMap<>();

请注意,在泛型类实例化期间利用类型推断,必须使用菱形。在以下示例中,编译器生成了未经检查的转换警告,因为HashMap()构造函数引用了HashMap原始类型,而不是Map<String, List<String>>类型:

Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning

泛型和非泛型类的泛型构造函数的类型推断

请注意,构造函数可以是泛型的(换句话说,在泛型和非泛型类中声明自己的形式类型参数)。考虑以下示例:

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}

考虑类MyClass的以下实例化:

new MyClass<Integer>("")

这个语句创建了参数化类型MyClass<Integer>的实例;语句明确为泛型类MyClass<X>的形式类型参数X指定了类型Integer。请注意,这个泛型类的构造函数包含一个形式类型参数T。编译器为这个泛型类的构造函数的形式类型参数T推断了类型String(因为这个构造函数的实际参数是一个String对象)。

Java SE 7 之前的编译器能够推断泛型构造函数的实际类型参数,类似于泛型方法。然而,在 Java SE 7 及更高版本中,如果使用菱形(<>),编译器可以推断正在实例化的泛型类的实际类型参数。考虑以下例子:

MyClass<Integer> myObject = new MyClass<>("");

在这个例子中,编译器为泛型类MyClass<X>的形式类型参数X推断了类型Integer。它为这个泛型类的构造函数的形式类型参数T推断了类型String


注意:需要注意的是,推断算法仅使用调用参数、目标类型和可能的明显预期返回类型来推断类型。推断算法不使用程序后面的结果。


目标类型

Java 编译器利用目标类型推断泛型方法调用的类型参数。表达式的目标类型是 Java 编译器根据表达式出现的位置所期望的数据类型。考虑声明如下的方法Collections.emptyList

static <T> List<T> emptyList();

考虑以下赋值语句:

List<String> listOne = Collections.emptyList();

这个语句期望一个List<String>的实例;这个数据类型是目标类型。因为方法emptyList返回类型为List<T>的值,编译器推断类型参数T必须是值String。这在 Java SE 7 和 8 中都适用。或者,您可以使用类型推断并指定T的值如下:

List<String> listOne = Collections.<String>emptyList();

然而,在这种情况下并不是必需的。尽管在其他情况下是必需的。考虑以下方法:

void processStringList(List<String> stringList) {
    // process stringList
}

假设您想要使用空列表调用方法processStringList。在 Java SE 7 中,以下语句不会编译:

processStringList(Collections.emptyList());

Java SE 7 编译器生成类似以下的错误消息:

List<Object> cannot be converted to List<String>

编译器需要一个类型参数T的值,因此它从值Object开始。因此,调用Collections.emptyList返回一个类型为List<Object>的值,这与方法processStringList不兼容。因此,在 Java SE 7 中,您必须如下指定类型参数的值:

processStringList(Collections.<String>emptyList());

在 Java SE 8 中,这已经不再是必需的。什么是目标类型的概念已经扩展到包括方法参数,比如方法processStringList的参数。在这种情况下,processStringList需要一个类型为List<String>的参数。方法Collections.emptyList返回一个List<T>的值,因此使用List<String>的目标类型,编译器推断类型参数T的值为String。因此,在 Java SE 8 中,以下语句编译通过:

processStringList(Collections.emptyList());

查看目标类型在 Lambda 表达式中获取更多信息。

通配符

原文:docs.oracle.com/javase/tutorial/java/generics/wildcards.html

在泛型代码中,问号(?),称为通配符,表示未知类型。 通配符可以在各种情况下使用:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更具体的编程实践更好)。 通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。

以下部分将更详细地讨论通配符,包括上界通配符、下界通配符和通配符捕获。

上界通配符

原文:docs.oracle.com/javase/tutorial/java/generics/upperBounded.html

您可以使用上界通配符来放宽对变量的限制。例如,假设您想编写一个适用于List<Integer>List<Double>List<Number>的方法;您可以通过使用上界通配符来实现这一点。

要声明上界通配符,请使用通配符字符('?'),后跟extends关键字,再跟其上界。请注意,在此上下文中,extends的含义是广义上的,既可以表示"extends"(如类)也可以表示"implements"(如接口)。

要编写适用于Number及其子类型(如IntegerDoubleFloat)的列表的方法,您应指定List<? extends Number>。术语List<Number>List<? extends Number>更为严格,因为前者仅匹配类型为Number的列表,而后者匹配类型为Number或其任何子类的列表。

考虑以下process方法:

public static void process(List<? extends Foo> list) { /* ... */ }

上界通配符<? extends Foo>,其中Foo是任何类型,匹配FooFoo的任何子类型。process方法可以将列表元素作为类型Foo访问:

public static void process(List<? extends Foo> list) {
    for (Foo elem : list) {
        // ...
    }
}

foreach子句中,elem变量遍历列表中的每个元素。现在可以在elem上使用Foo类中定义的任何方法。

sumOfList方法返回列表中数字的总和:

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

使用Integer对象列表的以下代码打印sum = 6.0

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

一组Double值可以使用相同的sumOfList方法。以下代码打印sum = 7.0

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));

无界通配符

原文:docs.oracle.com/javase/tutorial/java/generics/unboundedWildcards.html

未限定通配符类型是使用通配符字符(?)指定的,例如,List<?>。这被称为未知类型的列表。有两种情况下未限定通配符是一个有用的方法:

  • 如果你正在编写一个可以使用Object类提供的功能来实现的方法。

  • 当代码使用泛型类中不依赖于类型参数的方法时。例如,List.sizeList.clear。事实上,Class<?>经常被使用,因为Class<T>中的大多数方法不依赖于T

考虑以下方法,printList

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

printList的目标是打印任何类型的列表,但它未能实现这个目标 — 它只打印Object实例的列表;它无法打印List<Integer>List<String>List<Double>等,因为它们不是List<Object>的子类型。要编写一个通用的printList方法,使用List<?>

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

因为对于任何具体类型AList<A>List<?>的子类型,所以你可以使用printList来打印任何类型的列表:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);


注意: 在本课程的示例中使用了Arrays.asList方法。这个静态工厂方法将指定的数组转换并返回一个固定大小的列表。


需要注意的是,List<Object>List<?>并不相同。你可以将ObjectObject的任何子类型插入List<Object>中。但你只能将null插入List<?>中。通配符使用指南部分有关于如何确定在特定情况下应该使用什么类型的通配符的更多信息。

下界通配符

原文:docs.oracle.com/javase/tutorial/java/generics/lowerBounded.html

上界通配符 部分显示,上界通配符将未知类型限制为特定类型或该类型的子类型,并使用 extends 关键字表示。类似地,下界 通配符将未知类型限制为特定类型或该类型的 超类型

使用通配符字符('?')表示下界通配符,后跟 super 关键字,再跟着其 下界<? super A>


注意: 你可以为通配符指定上界,也可以指定下界,但不能同时指定两者。


假设你想编写一个将 Integer 对象放入列表的方法。为了最大限度地提高灵活性,你希望该方法适用于 List<Integer>List<Number>List<Object> — 任何可以容纳 Integer 值的东西。

要编写适用于 Integer 列表和 Integer 的超类型(如 IntegerNumberObject)的方法,你应该指定 List<? super Integer>。术语 List<Integer>List<? super Integer> 更具限制性,因为前者仅匹配类型为 Integer 的列表,而后者匹配任何是 Integer 超类型的列表。

以下代码将数字 1 到 10 添加到列表的末尾:

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

通配符使用指南 部分提供了何时使用上界通配符和何时使用下界通配符的指导。

通配符和子类型

原文:docs.oracle.com/javase/tutorial/java/generics/subtyping.html

如泛型、继承和子类型中所述,泛型类或接口之间并不仅仅因为它们的类型之间存在关系而相关。但是,您可以使用通配符来创建泛型类或接口之间的关系。

给定以下两个常规(非泛型)类:

class A { /* ... */ }
class B extends A { /* ... */ }

编写以下代码是合理的:

B b = new B();
A a = b;

此示例显示了常规类的继承遵循子类型规则:如果B扩展A,则类B是类A的子类型。这个规则不适用于泛型类型:

List<B> lb = new ArrayList<>();
List<A> la = lb;   // compile-time error

鉴于IntegerNumber的子类型,那么List<Integer>List<Number>之间的关系是什么?

显示 List和 List的共同父类是未知类型列表的图表共同父类是List<?>

尽管IntegerNumber的子类型,但List<Integer>不是List<Number>的子类型,实际上,这两种类型没有关系。List<Number>List<Integer>的共同父类是List<?>

为了创建这些类之间的关系,以便代码可以通过List<Integer>的元素访问Number的方法,请使用上界通配符:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

因为IntegerNumber的子类型,而numListNumber对象的列表,现在intListInteger对象的列表)和numList之间存在关系。以下图表显示了使用上下界通配符声明的几个List类之间的关系。

显示 List是 List的子类型。List是 List的子类型。几个泛型List类声明的层次结构。

通配符使用指南部分有关于使用上下界通配符的更多信息。

通配符捕获和辅助方法

原文:docs.oracle.com/javase/tutorial/java/generics/capture.html

在某些情况下,编译器会推断通配符的类型。例如,一个列表可能被定义为List<?>,但在评估表达式时,编译器会从代码中推断出特定的类型。这种情况被称为通配符捕获

对于大多数情况,你不需要担心通配符捕获,除非你看到一个包含短语“capture of”的错误消息。

当编译时,WildcardError示例会产生一个捕获错误:

import java.util.List;

public class WildcardError {

    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}

在这个例子中,编译器将i输入参数处理为Object类型。当foo方法调用List.set(int, E)时,编译器无法确认要插入列表的对象类型,从而产生错误。当出现这种类型的错误时,通常意味着编译器认为你正在将错误的类型赋给变量。泛型被添加到 Java 语言中的原因就是为了在编译时强制执行类型安全。

WildcardError示例在 Oracle 的 JDK 7 javac实现编译时生成以下错误:

WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
    i.set(0, i.get(0));
     ^
  required: int,CAP#1
  found: int,Object
  reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error

在这个例子中,代码正在尝试执行一个安全的操作,那么你如何解决编译器错误呢?你可以通过编写一个私有辅助方法来修复它,该方法捕获通配符。在这种情况下,你可以通过创建私有辅助方法fooHelper来解决问题,如WildcardFixed中所示:

public class WildcardFixed {

    void foo(List<?> i) {
        fooHelper(i);
    }

    // Helper method created so that the wildcard can be captured
    // through type inference.
    private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }

}

多亏了辅助方法,编译器使用推断确定TCAP#1,即捕获变量,在调用中。现在的示例成功编译。

按照惯例,辅助方法通常被命名为*originalMethodName*Helper

现在考虑一个更复杂的例子,WildcardErrorBad

import java.util.List;

public class WildcardErrorBad {

    void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
      Number temp = l1.get(0);
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
                            // got a CAP#2 extends Number;
                            // same bound, but different types
      l2.set(0, temp);	    // expected a CAP#1 extends Number,
                            // got a Number
    }
}

在这个例子中,代码正在尝试一个不安全的操作。例如,考虑swapFirst方法的以下调用:

List<Integer> li = Arrays.asList(1, 2, 3);
List<Double>  ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);

虽然List<Integer>List<Double>都满足List<? extends Number>的条件,但从Integer值列表中取一个项目并尝试将其放入Double值列表中显然是不正确的。

使用 Oracle 的 JDK javac编译器编译代码会产生以下错误:

WildcardErrorBad.java:7: error: method set in interface List<E> cannot be applied to given types;
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
        ^
  required: int,CAP#1
  found: int,Number
  reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:10: error: method set in interface List<E> cannot be applied to given types;
      l2.set(0, temp);      // expected a CAP#1 extends Number,
        ^
  required: int,CAP#1
  found: int,Number
  reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:15: error: method set in interface List<E> cannot be applied to given types;
        i.set(0, i.get(0));
         ^
  required: int,CAP#1
  found: int,Object
  reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
3 errors

没有辅助方法可以解决这个问题,因为代码本质上是错误的:从Integer值列表中取一个项目并尝试将其放入Double值列表中显然是不正确的。

通配符使用准则

原文:docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html

在学习如何使用泛型编程时,确定何时使用上界通配符和何时使用下界通配符是更令人困惑的方面之一。本页面提供了一些在设计代码时遵循的准则。

在本讨论中,将变量视为提供两种功能之一是有帮助的:

一个“In”变量

一个“in”变量向代码提供数据。想象一个带有两个参数的复制方法:copy(src, dest)src参数提供要复制的数据,因此它是“in”参数。

一个“Out”变量

一个“out”变量保存数据以供其他地方使用。在复制示例中,copy(src, dest)dest参数接受数据,因此它是“out”参数。

当然,有些变量既用于“in”又用于“out” —— 这种情况也在准则中有所涉及。

在决定是否使用通配符以及何种类型的通配符适合时,可以使用“in”和“out”原则。以下列表提供了要遵循的准则:


通配符准则:

  • 使用extends关键字定义具有上界通配符的“in”变量。

  • 使用super关键字定义具有下界通配符的“out”变量。

  • 在“in”变量可以使用在Object类中定义的方法访问的情况下,使用无界通配符。

  • 在代码需要将变量作为“in”和“out”变量访问的情况下,不要使用通配符。


这些准则不适用于方法的返回类型。应避免将通配符用作返回类型,因为这会强迫使用代码的程序员处理通配符。

List<? extends ...>定义的列表可以非正式地被视为只读的,但这并不是一个严格的保证。假设你有以下两个类:

class NaturalNumber {

    private int i;

    public NaturalNumber(int i) { this.i = i; }
    // ...
}

class EvenNumber extends NaturalNumber {

    public EvenNumber(int i) { super(i); }
    // ...
}

考虑以下代码:

List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35));  // compile-time error

因为List<EvenNumber>List<? extends NaturalNumber>的子类型,所以你可以将le赋给ln。但你不能使用ln向偶数列表添加自然数。列表上的以下操作是可能的:

  • 你可以添加null

  • 你可以调用clear

  • 你可以获取迭代器并调用remove

  • 你可以捕获通配符并写入你从列表中读取的元素。

你可以看到由List<? extends NaturalNumber>定义的列表在严格意义上并不是只读的,但你可能会这样认为,因为你不能在列表中存储新元素或更改现有元素。

类型擦除

原文:docs.oracle.com/javase/tutorial/java/generics/erasure.html

泛型被引入 Java 语言,以在编译时提供更严格的类型检查,并支持泛型编程。为了实现泛型,Java 编译器对其应用类型擦除:

  • 将泛型类型中的所有类型参数替换为它们的边界或Object(如果类型参数是无界的)。因此生成的字节码只包含普通类、接口和方法。

  • 如有必要,插入类型转换以保持类型安全。

  • 生成桥接方法以保留扩展泛型类型中的多态性。

类型擦除确保为参数化类型不会创建新类;因此,泛型不会产生运行时开销。

泛型类型的擦除

原文:docs.oracle.com/javase/tutorial/java/generics/genTypes.html

在类型擦除过程中,Java 编译器擦除所有类型参数,并用其第一个边界替换每个类型参数(如果类型参数是有界的),或者用Object替换(如果类型参数是无界的)。

考虑下面表示单链表中节点的泛型类:

public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

因为类型参数T是无界的,Java 编译器将其替换为Object

public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

在下面的示例中,泛型Node类使用了有界类型参数:

public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

Java 编译器将有界类型参数T替换为第一个边界类Comparable

public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

擦除通用方法

原文:docs.oracle.com/javase/tutorial/java/generics/genMethods.html

Java 编译器还会擦除通用方法参数中的类型参数。考虑以下通用方法:

// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

因为T是无界的,Java 编译器将其替换为Object

public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

假设以下类已被定义:

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

您可以编写一个通用方法来绘制不同的形状:

public static <T extends Shape> void draw(T shape) { /* ... */ }

Java 编译器将T替换为Shape

public static void draw(Shape shape) { /* ... */ }

类型擦除和桥接方法的影响

原文:docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html

有时类型擦除会导致一个你可能没有预料到的情况。以下示例展示了这种情况是如何发生的。有时编译器会在类型擦除过程中创建一个合成方法,称为桥接方法

给定以下两个类:

public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

考虑以下代码:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
n.setData("Hello");     // Causes a ClassCastException to be thrown.
Integer x = mn.data;    

经过类型擦除后,这段代码变成:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
                        // Note: This statement could instead be the following:
                        //     Node n = (Node)mn;
                        // However, the compiler doesn't generate a cast because
                        // it isn't required.
n.setData("Hello");     // Causes a ClassCastException to be thrown.
Integer x = (Integer)mn.data; 

下一节将解释为什么在 n.setData("Hello"); 语句处抛出 ClassCastException

桥接方法

当编译一个继承参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。通常情况下,你不需要担心桥接方法,但如果在堆栈跟踪中出现一个,你可能会感到困惑。

经过类型擦除后,NodeMyNode 类变成:

public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

经过类型擦除后,方法签名不匹配;Node.setData(T) 方法变成了 Node.setData(Object)。因此,MyNode.setData(Integer) 方法不会覆盖 Node.setData(Object) 方法。

为了解决这个问题并在类型擦除后保留泛型类型的多态性,Java 编译器生成一个桥接方法来确保子类型化按预期工作。

对于 MyNode 类,编译器为 setData 生成了以下桥接方法:

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

桥接方法 MyNode.setData(object) 委托给原始的 MyNode.setData(Integer) 方法。因此,n.setData("Hello"); 语句调用了 MyNode.setData(Object) 方法,由于 "Hello" 无法转换为 Integer,导致抛出 ClassCastException

非可实例化类型

原文:docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html

章节 类型擦除 讨论了编译器删除与类型参数和类型参数相关的信息的过程。类型擦除对于具有非可实例化类型的可变参数(也称为 varargs)方法有相关后果。有关可变参数方法的更多信息,请参见 传递信息给方法或构造函数 中的 任意数量的参数 章节。

本页涵盖以下主题:

  • 非可实例化类型

  • 堆污染

  • 具有非可实例化形式参数的可变参数方法的潜在漏洞

  • 防止具有非可实例化形式参数的可变参数方法产生警告

非可实例化类型

可实例化 类型是一种在运行时完全可用的类型信息的类型。这包括原始类型、非泛型类型、原始类型和未绑定通配符的调用。

非可实例化类型 是在编译时通过类型擦除删除了信息的类型 —— 未定义为未限定通配符的泛型类型的调用。非可实例化类型在运行时不具备所有信息。非可实例化类型的示例包括 List<String>List<Number>;JVM 无法在运行时区分这些类型。如 泛型的限制 所示,有一些情况下不能使用非可实例化类型:例如,在 instanceof 表达式中,或作为数组中的元素。

堆污染

堆污染 发生在参数化类型的变量引用不是该参数化类型的对象时。如果程序执行了一些操作导致在编译时产生未经检查的警告,则会出现这种情况。如果在编译时(在编译时类型检查规则的限制范围内)或在运行时无法验证涉及参数化类型的操作的正确性(例如,强制转换或方法调用),则会生成 未经检查的警告。例如,当混合使用原始类型和参数化类型,或执行未经检查的强制转换时,就会发生堆污染。

在正常情况下,当所有代码同时编译时,编译器会发出未经检查的警告,以引起您对潜在的堆污染的注意。如果您分别编译代码的各个部分,很难检测到堆污染的潜在风险。如果确保您的代码在没有警告的情况下编译通过,那么就不会发生堆污染。

具有非可实例化形式参数的可变参数方法的潜在漏洞

包含可变参数输入参数的泛型方法可能导致堆污染。

考虑以下ArrayBuilder类:

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }

}

以下示例HeapPollutionExample使用了ArrayBuiler类:

public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists =
      new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,
      stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

编译时,ArrayBuilder.addToList方法的定义会产生以下警告:

warning: [varargs] Possible heap pollution from parameterized vararg type T

当编译器遇到可变参数方法时,它将可变参数形式参数转换为数组。然而,Java 编程语言不允许创建参数化类型的数组。在方法ArrayBuilder.addToList中,编译器将可变参数形式参数T... elements转换为形式参数T[] elements,一个数组。然而,由于类型擦除,编译器将可变参数形式参数转换为Object[] elements。因此,存在堆污染的可能性。

以下语句将可变参数形式参数l赋给Object数组objectArgs

Object[] objectArray = l;

这个语句可能会引入堆污染。一个与可变参数形式参数l的参数化类型不匹配的值可以赋给变量objectArray,从而可以赋给l。然而,在这个语句中,编译器并不生成未经检查的警告。编译器在将可变参数形式参数List<String>... l翻译为形式参数List[] l时已经生成了警告。这个语句是有效的;变量l的类型是List[],它是Object[]的子类型。

因此,如果您将任何类型的List对象分配给objectArray数组的任何数组组件,编译器不会发出警告或错误,如下所示:

objectArray[0] = Arrays.asList(42);

这个语句将包含一个类型为Integer的对象的List对象分配给objectArray数组的第一个数组组件。

假设您使用以下语句调用ArrayBuilder.faultyMethod

ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));

在运行时,JVM 在以下语句处抛出ClassCastException

// ClassCastException thrown here
String s = l[0].get(0);

存储在变量l的第一个数组组件中的对象的类型是List<Integer>,但这个语句期望的是类型为List<String>的对象。

防止具有非可重复形式参数的可变参数方法产生警告

如果您声明一个具有参数化类型参数的可变参数方法,并确保方法体不会因为对可变参数形式参数的不当处理而抛出ClassCastException或其他类似异常,您可以通过在静态和非构造方法声明中添加以下注解来防止编译器为这些类型的可变参数方法生成警告:

@SafeVarargs

@SafeVarargs注解是方法契约的一部分;此注解断言方法的实现不会不当处理可变参数形式参数。

也可以通过在方法声明中添加以下内容来抑制此类警告,尽管这种做法不太理想:

@SuppressWarnings({"unchecked", "varargs"})

然而,这种方法并不会抑制从方法调用点生成的警告。如果你对@SuppressWarnings语法不熟悉,请参见 Annotations。

泛型的限制。

原文:docs.oracle.com/javase/tutorial/java/generics/restrictions.html

要有效地使用 Java 泛型,您必须考虑以下限制:

  • 不能用原始类型实例化泛型类型。

  • 不能创建类型参数的实例。

  • 不能声明其类型为类型参数的静态字段。

  • 不能在参数化类型中使用强制类型转换或instanceof

  • 不能创建参数化类型的数组。

  • 不能创建、捕获或抛出参数化类型的对象。

  • 不能重载形式参数类型擦除为相同原始类型的方法。

不能用原始类型实例化泛型类型。

考虑以下参数化类型:

class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    // ...
}

在创建Pair对象时,您不能用原始类型替换类型参数KV

Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error

您只能用非原始类型替换类型参数KV

Pair<Integer, Character> p = new Pair<>(8, 'a');

请注意,Java 编译器会将8自动装箱为Integer.valueOf(8),将'a'自动装箱为Character('a')

Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));

有关自动装箱的更多信息,请参见自动装箱和拆箱中的数字和字符串课程。

不能创建类型参数的实例。

你不能创建一个类型参数的实例。例如,以下代码会导致编译时错误:

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

作为一种解决方法,您可以通过反射创建一个类型参数的对象:

public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}

您可以按以下方式调用append方法:

List<String> ls = new ArrayList<>();
append(ls, String.class);

不能声明其类型为类型参数的静态字段。

类的静态字段是所有非静态对象共享的类级变量。因此,不允许类型参数的静态字段。考虑以下类:

public class MobileDevice<T> {
    private static T os;

    // ...
}

如果允许类型参数的静态字段,则以下代码将会混淆:

MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

因为静态字段osphonepagerpc共享,os的实际类型是什么?它不能同时是SmartphonePagerTabletPC。因此,您不能创建类型参数的静态字段。

不能在参数化类型中使用强制类型转换或instanceof

因为 Java 编译器会擦除泛型代码中的所有类型参数,所以无法在运行时验证泛型类型的参数化类型:

public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}

传递给rtti方法的参数化类型集合为:

S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }

运行时不会跟踪类型参数,因此无法区分ArrayList<Integer>ArrayList<String>之间的区别。您最多可以使用无界通配符来验证列表是否是ArrayList

public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}

通常情况下,除非使用无界通配符进行参数化,否则不能进行参数化类型的强制转换。例如:

List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // compile-time error

但是,在某些情况下,编译器知道类型参数始终有效并允许强制转换。例如:

List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1;  // OK

不能创建参数化类型的数组。

你不能创建参数化类型的数组。例如,以下代码无法编译:

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

以下代码说明了当不同类型插入数组时会发生什么:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

如果你尝试对一个泛型列表做同样的事情,会出现问题:

Object[] stringLists = new List<String>[2];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                            // but the runtime can't detect it.

如果允许参数化列表的数组,上述代码将无法抛出期望的ArrayStoreException

无法创建、捕获或抛出参数化类型的对象

一个泛型类不能直接或间接地扩展Throwable类。例如,以下类将无法编译:

// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }    // compile-time error

// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error

一个方法无法捕获类型参数的实例:

public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   // compile-time error
        // ...
    }
}

但是,你可以在throws子句中使用类型参数:

class Parser<T extends Exception> {
    public void parse(File file) throws T {     // OK
        // ...
    }
}

无法重载形式参数类型擦除为相同原始类型的方法

一个类不能有两个在类型擦除后具有相同签名的重载方法。

public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}

这些重载将共享相同的类文件表示,并将生成编译时错误。

问题和练习:泛型

原文:docs.oracle.com/javase/tutorial/java/generics/QandE/generics-questions.html

  1. 编写一个通用方法来计算集合中具有特定属性的元素数量(例如,奇数、质数、回文数)。

  2. 以下类会编译吗?如果不会,为什么?

    public final class Algorithm {
        public static <T> T max(T x, T y) {
            return x > y ? x : y;
        }
    }
    
    
  3. 编写一个通用方法来交换数组中两个不同元素的位置。

  4. 如果编译器在编译时擦除所有类型参数,为什么应该使用泛型?

  5. 在类型擦除后,以下类被转换为什么?

    public class Pair<K, V> {
    
        public Pair(K key, V value) {
            this.key = key;
            this.value = value;
        }
    
        public K getKey() { return key; }
        public V getValue() { return value; }
    
        public void setKey(K key)     { this.key = key; }
        public void setValue(V value) { this.value = value; }
    
        private K key;
        private V value;
    }
    
    
  6. 以下方法在类型擦除后会转换为什么?

    public static <T extends Comparable<T>>
        int findFirstGreaterThan(T[] at, T elem) {
        // ...
    }
    
    
  7. 以下方法会编译吗?如果不会,为什么?

    public static void print(List<? extends Number> list) {
        for (Number n : list)
            System.out.print(n + " ");
        System.out.println();
    }
    
    
  8. 编写一个通用方法来查找列表范围begin, end)中的最大元素。

  9. 以下类会编译吗?如果不会,为什么?

    public class Singleton<T> {
    
        public static T getInstance() {
            if (instance == null)
                instance = new Singleton<T>();
    
            return instance;
        }
    
        private static T instance = null;
    }
    
    
  10. 给定以下类:

    class Shape { /* ... */ }
    class Circle extends Shape { /* ... */ }
    class Rectangle extends Shape { /* ... */ }
    
    class Node<T> { /* ... */ }
    
    

    以下代码会编译吗?如果不会,为什么?

    Node<Circle> nc = new Node<>();
    Node<Shape>  ns = nc;
    
    
  11. 考虑这个类:

    class Node<T> implements Comparable<T> {
        public int compareTo(T obj) { /* ... */ }
        // ...
    }
    
    

    以下代码会编译吗?如果不会,为什么?

    Node<String> node = new Node<>();
    Comparable<String> comp = node;
    
    
  12. 如何调用以下方法来找到列表中与指定整数列表互质的第一个整数?

    public static <T>
        int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p)
    
    

    注意,两个整数ab互质,如果 gcd(a, b) = 1,其中 gcd 是最大公约数的缩写。

[检查你的答案。

课程:包(Packages)

原文:docs.oracle.com/javase/tutorial/java/package/index.html

这节课讲解了如何将类和接口打包成包,如何使用在包中的类,以及如何安排文件系统以便编译器能够找到你的源文件。

创建和使用包

原文:docs.oracle.com/javase/tutorial/java/package/packages.html

为了使类型更容易找到和使用,避免命名冲突,并控制访问权限,程序员将相关类型的组合打包成包。


定义: 是提供访问保护和命名空间管理的相关类型的分组。请注意,类型 指的是类、接口、枚举和注解类型。枚举和注解类型是类和接口的特殊种类,因此在本课程中,类型 经常简称为类和接口


Java 平台中的类型是通过功能将类打包在一起的各种包的成员:基本类在java.lang中,用于读写(输入和输出)的类在java.io中,依此类推。你也可以将你的类型放在包中。

假设你编写了一组表示图形对象的类,比如圆、矩形、线条和点。你还编写了一个接口,Draggable,如果类可以被鼠标拖动,则实现该接口。

//*in the Draggable.java file*
public interface Draggable {
    ...
}

//*in the Graphic.java file*
public abstract class Graphic {
    ...
}

//*in the Circle.java file*
public class Circle extends Graphic
    implements Draggable {
    . . .
}

//*in the Rectangle.java file*
public class Rectangle extends Graphic
    implements Draggable {
    . . .
}

//*in the Point.java file*
public class Point extends Graphic
    implements Draggable {
    . . .
}

//*in the Line.java file*
public class Line extends Graphic
    implements Draggable {
    . . .
}

你应该将这些类和接口打包到一个包中,原因包括以下几点:

  • 你和其他程序员可以轻松确定这些类型是相关的。

  • 你和其他程序员知道在哪里找到可以提供与图形相关功能的类型。

  • 你的类型名称不会与其他包中的类型名称冲突,因为包会创建一个新的命名空间。

  • 你可以允许包内的类型彼此之间具有无限制的访问,但仍然限制包外类型的访问。

创建一个包

原文:docs.oracle.com/javase/tutorial/java/package/createpkgs.html

要创建一个包,你需要为包选择一个名称(命名约定将在下一节讨论),并在包含你想要放入包中的类型(类、接口、枚举和注解类型)的每个源文件的顶部放置一个带有该名称的package语句。

包语句(例如,package graphics;)必须是源文件中的第一行。每个源文件中只能有一个包语句,并且它适用于文件中的所有类型。


注意:如果你在单个源文件中放入多个类型,只能有一个是public的,并且它必须与源文件同名。例如,你可以在文件Circle.java中定义public class Circle,在文件Draggable.java中定义public interface Draggable,在文件Day.java中定义public enum Day,等等。

你可以在同一个文件中包含非公共类型和一个公共类型(这是强烈不推荐的,除非非公共类型很小并且与公共类型密切相关),但只有公共类型可以从包外访问。所有顶级的非公共类型将是包私有的。


如果你将前面部分列出的图形接口和类放在一个名为graphics的包中,你将需要六个源文件,如下所示:

//*in the Draggable.java file*
package graphics;
public interface Draggable {
    . . .
}

//*in the Graphic.java file*
package graphics;
public abstract class Graphic {
    . . .
}

//*in the Circle.java file*
package graphics;
public class Circle extends Graphic
    implements Draggable {
    . . .
}

//*in the Rectangle.java file*
package graphics;
public class Rectangle extends Graphic
    implements Draggable {
    . . .
}

//*in the Point.java file*
package graphics;
public class Point extends Graphic
    implements Draggable {
    . . .
}

//*in the Line.java file*
package graphics;
public class Line extends Graphic
    implements Draggable {
    . . .
}

如果你不使用package语句,你的类型将会进入一个无名包。一般来说,无名包只适用于小型或临时应用程序,或者当你刚开始开发过程时。否则,类和接口应该放在命名包中。

包命名

原文:docs.oracle.com/javase/tutorial/java/package/namingpkgs.html

全球范围内的程序员使用 Java 编程语言编写类和接口,很可能许多程序员会为不同类型使用相同的名称。事实上,前面的例子就是这样做的:它定义了一个 Rectangle 类,而 java.awt 包中已经有一个 Rectangle 类。尽管如此,如果它们位于不同的包中,编译器仍允许两个类具有相同的名称。每个 Rectangle 类的完全限定名称包括包名。也就是说,graphics 包中的 Rectangle 类的完全限定名称是 graphics.Rectangle,而 java.awt 包中的 Rectangle 类的完全限定名称是 java.awt.Rectangle

这种方法很有效,除非两个独立的程序员使用相同的包名。如何避免这个问题?约定。

命名约定

包名全部小写以避免与类或接口的名称冲突。

公司使用其反转的互联网域名作为其包名的起始部分—例如,com.example.mypackage 表示由 example.com 的程序员创建的名为 mypackage 的包。

公司内部发生的名称冲突需要在公司内部通过约定处理,也许可以在公司名称后面加上地区或项目名称(例如,com.example.region.mypackage)。

Java 语言中的包以 java.javax. 开头。

在某些情况下,互联网域名可能不是有效的包名。如果域名包含连字符或其他特殊字符,如果包名以数字或其他 Java 名称不允许用作 Java 名称开头的字符开头,或者包名包含保留的 Java 关键字,例如 "int"。在这种情况下,建议的约定是添加下划线。例如:

合法化包名

域名 包名前缀
hyphenated-name.example.org org.example.hyphenated_name
example.int int_.example
123name.example.com com.example._123name

使用包成员

原文:docs.oracle.com/javase/tutorial/java/package/usepkgs.html

组成包的类型被称为包成员

要从其包外部使用public包成员,你必须执行以下操作之一:

  • 通过其完全限定名称引用成员

  • 导入包成员

  • 导入成员的整个包

每种情况都适用于不同的情况,如下面的部分所解释的。

通过其限定名称引用包成员

到目前为止,在本教程中的大多数示例都通过其简单名称引用类型,如RectangleStackOfInts。如果你编写的代码与该成员在同一个包中,或者已经导入了该成员,你可以使用包成员的简单名称。

但是,如果你尝试使用来自不同包的成员,并且该包尚未被导入,你必须使用成员的完全限定名称,其中包括包名称。以下是在前面示例中声明的graphics包中的Rectangle类的完全限定名称。

graphics.Rectangle

你可以使用这个限定名称来创建graphics.Rectangle的实例:

graphics.Rectangle myRect = new graphics.Rectangle();

对于不经常使用的限定名称是可以的。然而,当一个名称被重复使用时,反复输入名称变得乏味,代码变得难以阅读。作为替代方案,你可以导入成员或其包,然后使用其简单名称。

导入包成员

要将特定成员导入当前文件,请在文件开头放置一个import语句,在任何类型定义之前,但在package语句之后(如果有的话)。以下是如何从前一节中创建的graphics包中导入Rectangle类。

import graphics.Rectangle;

现在你可以通过其简单名称引用Rectangle类。

Rectangle myRectangle = new Rectangle();

如果你只从graphics包中使用少量成员,这种方法很有效。但如果你从一个包中使用许多类型,你应该导入整个包。

导入整个包

要导入特定包中包含的所有类型,请使用带有星号(*)通配符的import语句。

import graphics.*;

现在你可以通过其简单名称引用graphics包中的任何类或接口。

Circle myCircle = new Circle();
Rectangle myRectangle = new Rectangle();

import语句中的星号只能用于指定包中的所有类,如下所示。它不能用于匹配包中的一部分类。例如,以下内容不匹配以A开头的graphics包中的所有类。

// *does not work*
import graphics.A*;

相反,它会生成编译器错误。通常情况下,使用import语句只导入单个包成员或整个包。


注意: 另一种不太常见的 import 形式允许你导入封闭类的公共嵌套类。例如,如果 graphics.Rectangle 类包含有用的嵌套类,比如 Rectangle.DoubleWideRectangle.Square,你可以通过以下两个语句导入 Rectangle 及其嵌套类。

import graphics.Rectangle;
import graphics.Rectangle.*;

请注意,第二个导入语句不会导入 Rectangle

另一种不太常见的 import 形式,即静态导入语句,将在本节末尾讨论。


为了方便起见,Java 编译器会自动为每个源文件导入两个完整的包:(1)java.lang 包和(2)当前包(当前文件的包)。

包的表面层次结构

起初,包看起来是分层的,但实际上并非如此。例如,Java API 包括一个 java.awt 包,一个 java.awt.color 包,一个 java.awt.font 包,以及许多以 java.awt 开头的其他包。然而,java.awt.color 包、java.awt.font 包和其他 java.awt.xxxx不包含java.awt 包中。前缀 java.awt(Java 抽象窗口工具包)用于一些相关的包,以明确显示它们之间的关系,而不是表示包含关系。

导入 java.awt.* 导入了 java.awt 包中的所有类型,但不会导入 java.awt.colorjava.awt.font 或任何其他 java.awt.xxxx 包。如果你计划使用 java.awt.color 中的类以及 java.awt 中的类,你必须导入这两个包及其所有文件:

import java.awt.*;
import java.awt.color.*;

名称歧义

如果一个包中的成员与另一个包中的成员同名,并且两个包都被导入,你必须通过其限定名称引用每个成员。例如,graphics 包定义了一个名为 Rectangle 的类。java.awt 包也包含一个 Rectangle 类。如果 graphicsjava.awt 都被导入,以下内容是模棱两可的。

Rectangle rect;

在这种情况下,你必须使用成员的完全限定名称来指示你想要的确切 Rectangle 类。例如,

graphics.Rectangle rect;

静态导入语句

有些情况下,你需要频繁访问一个或两个类的静态 final 字段(常量)和静态方法。反复添加这些类的名称可能会导致代码混乱。静态导入语句为你提供了一种导入你想要使用的常量和静态方法的方式,这样你就不需要为它们的类名添加前缀。

java.lang.Math 类定义了 PI 常量和许多静态方法,包括用于计算正弦、余弦、正切、平方根、最大值、最小值、指数等的方法。例如,

public static final double PI 
    = 3.141592653589793;
public static double cos(double a)
{
    ...
}

通常,要从另一个类中使用这些对象,你需要添加类名前缀,如下所示。

double r = Math.cos(Math.PI * theta);

使用静态导入语句可以导入 java.lang.Math 的静态成员,这样就不需要在类名Math前加前缀了。Math的静态成员可以单独导入:

import static java.lang.Math.PI;

或者作为一个组:

import static java.lang.Math.*;

一旦它们被导入,静态成员可以无需限定地使用。例如,前面的代码片段将变为:

double r = cos(PI * theta);

显然,你可以编写自己的类,其中包含你经常使用的常量和静态方法,然后使用静态导入语句。例如,

import static mypackage.MyConstants.*;


注意: 静态导入要非常谨慎使用。过度使用静态导入会导致代码难以阅读和维护,因为代码读者无法知道哪个类定义了特定的静态对象。正确使用静态导入可以通过消除类名重复使代码更易读。


管理源文件和类文件

原文:docs.oracle.com/javase/tutorial/java/package/managingfiles.html

Java 平台的许多实现依赖于分层文件系统来管理源文件和类文件,尽管Java 语言规范并不要求这样做。策略如下。

将一个类、接口、枚举或注解类型的源代码放在一个文本文件中,文件名为类型的简单名称,扩展名为.java。例如:

//in the Rectangle.java file 
package graphics;
public class Rectangle {
   ... 
}

然后,将源文件放在一个反映类型所属包名的目录中:

.....\graphics\Rectangle.java

包成员的限定名称和文件的路径名称是平行的,假设使用 Microsoft Windows 文件名分隔符反斜杠(对于 UNIX,请使用正斜杠)。

  • 类名graphics.Rectangle

  • 文件路径graphics\Rectangle.java

正如你应该记得的那样,按照惯例,公司使用其反转的互联网域名作为其包名。例如,其互联网域名为example.com的 Example 公司将在其所有包名之前加上com.example。包名的每个组件对应一个子目录。因此,如果 Example 公司有一个包含Rectangle.java源文件的com.example.graphics包,它将包含在一系列子目录中,如下所示:

....\com\example\graphics\Rectangle.java

当你编译一个源文件时,编译器为其中定义的每个类型创建一个不同的输出文件。输出文件的基本名称是类型的名称,其扩展名是.class。例如,如果源文件如下所示

//in the Rectangle.java file
package com.example.graphics;
public class Rectangle {
      . . . 
}

class Helper{
      . . . 
}

然后编译后的文件将位于:

<path to the parent directory of the output files>\com\example\graphics\Rectangle.class
<path to the parent directory of the output files>\com\example\graphics\Helper.class

.java源文件一样,编译后的.class文件应该在反映包名的一系列目录中。然而,.class文件的路径不一定要与.java源文件的路径相同。你可以将源文件和类文件目录分开管理,如:

<path_one>\sources\com\example\graphics\Rectangle.java

<path_two>\classes\com\example\graphics\Rectangle.class

通过这样做,你可以将classes目录提供给其他程序员,而不会泄露你的源代码。你还需要以这种方式管理源文件和类文件,以便编译器和 Java 虚拟机(JVM)可以找到程序中使用的所有类型。

classes目录的完整路径,<path_two>\classes,被称为类路径,并通过CLASSPATH系统变量设置。编译器和 JVM 都会通过将包名添加到类路径来构建到你的.class文件的路径。例如,如果

<path_two>\classes

是你的类路径,包名是

com.example.graphics,

然后编译器和 JVM 会在以下位置查找.class文件

<path_two>\classes\com\example\graphics.

类路径可以包括多个路径,用分号(Windows)或冒号(UNIX)分隔。默认情况下,编译器和 JVM 会搜索当前目录和包含 Java 平台类的 JAR 文件,因此这些目录会自动包含在你的类路径中。

设置 CLASSPATH 系统变量

要在 Windows 和 UNIX(Bourne shell)中显示当前的CLASSPATH变量,请使用以下命令:

In Windows:   C:\> set CLASSPATH
In UNIX:      % echo $CLASSPATH

要删除当前CLASSPATH变量的内容,请使用以下命令:

In Windows:   C:\> set CLASSPATH=
In UNIX:      % unset CLASSPATH; export CLASSPATH

要设置CLASSPATH变量,请使用以下命令(例如):

In Windows:   C:\> set CLASSPATH=C:\users\george\java\classes
In UNIX:      % CLASSPATH=/home/george/java/classes; export CLASSPATH

创建和使用包的总结

原文:docs.oracle.com/javase/tutorial/java/package/summary-package.html

要为一个类型创建一个包,将一个package语句放在包含该类型(类、接口、枚举或注解类型)的源文件中的第一个语句位置。

要使用不同包中的公共类型,你有三种选择:(1)使用类型的完全限定名称,(2)导入类型,或者(3)导入包含该类型的整个包。

包的源文件和类文件的路径名称与包的名称相对应。

你可能需要设置你的CLASSPATH,以便编译器和 JVM 可以找到你的类型的.class文件。

问题和练习:创建和使用包

原文:docs.oracle.com/javase/tutorial/java/package/QandE/packages-questions.html

问题

假设你已经编写了一些类。突然间,你决定将它们分成三个包,如下表所列。此外,假设这些类当前位于默认包中(没有package语句)。

目标包

包名称 类名称
mygame.server Server
mygame.shared Utilities
mygame.client Client
  1. 你需要在每个源文件中添加哪行代码才能将每个类放在正确的包中?

  2. 为了遵循目录结构,你需要在开发目录中创建一些子目录,并将源文件放在正确的子目录中。你需要创建哪些子目录?每个源文件应放在哪个子目录中?

  3. 你认为你需要对源文件进行其他任何更改才能使它们正确编译吗?如果需要,是什么?

练习

下载列在这里的源文件。

  • Client

  • Server

  • Utilities

  1. 使用刚刚下载的源文件实现你在问题 1 到 3 中提出的更改。

  2. 编译修改后的源文件。(提示:如果你是从命令行调用编译器(而不是使用构建工具),请从包含你刚刚创建的mygame目录的目录中调用编译器。

检查你的答案。

Trail: Java 基础类

原文:docs.oracle.com/javase/tutorial/essential/index.html

本教程讨论了对大多数程序员至关重要的 Java 平台类。

解释了异常机制以及如何用它来处理错误和其他异常情况。本课程描述了异常是什么,如何抛出和捕获异常,一旦捕获异常后该如何处理,以及如何使用异常类层次结构。

包括用于基本输入和输出的 Java 平台类。它主要关注I/O 流,这是一个极大简化 I/O 操作的强大概念。本课程还介绍了序列化,它允许程序将整个对象写入流并再次读取它们。然后,课程介绍了一些文件系统操作,包括随机访问文件。最后,简要介绍了新 I/O API 的高级功能。

解释了如何编写能够同时执行多个任务的应用程序。Java 平台从头开始设计,以支持并发编程,在 Java 编程语言和 Java 类库中提供基本的并发支持。自 5.0 版本以来,Java 平台还包括高级并发 API。本课程介绍了平台的基本并发支持,并总结了java.util.concurrent包中一些高级 API。

是由底层操作系统、Java 虚拟机、类库和应用程序启动时提供的各种配置数据定义的。本课程描述了应用程序用于检查和配置其平台环境的一些 API。

是一种根据集合中每个字符串共享的共同特征描述字符串集合的方法。它们可用于搜索、编辑或操作文本和数据。正则表达式的复杂程度各不相同,但一旦理解了它们的构造基础,您就能解读(或创建)任何正则表达式。本课程教授了java.util.regex API 支持的正则表达式语法,并提供了几个工作示例以说明各种对象之间的交互方式。

课程:异常

原文:docs.oracle.com/javase/tutorial/essential/exceptions/index.html

Java 编程语言使用异常来处理错误和其他异常事件。本课程描述了何时以及如何使用异常。

什么是异常?

异常是程序执行过程中发生的事件,打断了指令的正常流程。

捕获或声明要求

这一部分涵盖了如何捕获和处理异常。讨论包括trycatchfinally块,以及链式异常和日志记录。

如何抛出异常

这一部分涵盖了throw语句和Throwable类及其子类。

try-with-resources 语句

这一部分描述了try-with-resources语句,它是一个声明一个或多个资源的try语句。资源是程序完成后必须关闭的对象。try-with-resources语句确保在语句结束时关闭每个资源。

未经检查的异常 - 争议

这一部分解释了由RuntimeException子类指示的未经检查异常的正确和不正确使用。

异常的优势

使用异常来管理错误相对于传统的错误管理技术有一些优势。您将在本节中了解更多信息。

总结

问题和练习

什么是异常?

原文:docs.oracle.com/javase/tutorial/essential/exceptions/definition.html

术语异常是“异常事件”的简称。


定义: 异常是程序执行过程中发生的事件,打断了程序正常指令流程。


当方法内部发生错误时,方法会创建一个对象并将其交给运行时系统。这个对象称为异常对象,包含有关错误的信息,包括错误发生时的类型和程序状态。创建异常对象并将其交给运行时系统称为抛出异常

方法抛出异常后,运行时系统会尝试寻找处理异常的方法。可以处理异常的一系列可能的“方法”是调用到发生错误的方法的有序方法列表。这些方法的列表称为调用堆栈(见下图)。

调用堆栈显示三个方法调用,其中第一个调用的方法具有异常处理程序。

调用堆栈。

运行时系统在调用堆栈中搜索包含可以处理异常的代码块的方法。这个代码块称为异常处理程序。搜索从发生错误的方法开始,并按照调用方法的相反顺序在调用堆栈中进行。当找到适当的处理程序时,运行时系统将异常传递给处理程序。如果抛出的异常对象的类型与处理程序可以处理的类型匹配,则认为异常处理程序是适当的。

选择的异常处理程序被称为捕获异常。如果运行时系统在调用堆栈上详尽搜索而找不到适当的异常处理程序,如下图所示,运行时系统(以及因此程序)将终止。

调用堆栈显示三个方法调用,其中第一个调用的方法具有异常处理程序。

在调用堆栈中搜索异常处理程序。

使用异常来管理错误相比传统的错误管理技术有一些优势。您可以在异常的优势部分了解更多信息。

捕获或指定要求

原文:docs.oracle.com/javase/tutorial/essential/exceptions/catchOrDeclare.html

有效的 Java 编程语言代码必须遵守捕获或指定要求。这意味着可能引发某些异常的代码必须被以下之一包围:

  • 一个捕获异常的try语句。try必须提供异常处理程序,如 Catching and Handling Exceptions 中所述。

  • 指定可能引发异常的方法。该方法必须提供列出异常的throws子句,如 Specifying the Exceptions Thrown by a Method 中所述。

未遵守捕获或指定要求的代码将无法编译。

并非所有异常都受捕获或指定要求的约束。要理解原因,我们需要看一下三种基本类别的异常,其中只有一种受到该要求的约束。

三种异常类型

第一种异常是受检异常。这些是一个良好编写的应用程序应该预料并从中恢复的异常情况。例如,假设一个应用程序提示用户输入文件名,然后通过将名称传递给java.io.FileReader的构造函数来打开文件。通常,用户提供现有的可读文件的名称,因此FileReader对象的构造成功,应用程序的执行正常进行。但有时用户提供不存在文件的名称,构造函数会抛出java.io.FileNotFoundException。一个良好编写的程序将捕获此异常并通知用户错误,可能提示更正的文件名。

受检异常受到捕获或指定要求的约束。所有异常都是受检异常,除了由ErrorRuntimeException及其子类指示的异常。

第二种异常是错误。这些是应用程序外部的异常情况,应用程序通常无法预料或从中恢复。例如,假设一个应用程序成功打开一个输入文件,但由于硬件或系统故障无法读取文件。读取失败将抛出java.io.IOError。应用程序可能选择捕获此异常,以通知用户问题,但程序打印堆栈跟踪并退出也是有道理的。

错误不受捕获或指定要求的约束。错误是由Error及其子类指示的异常。

第三种异常是运行时异常。这些是应用程序内部的异常情况,应用程序通常无法预料或恢复。这通常表示编程错误,如逻辑错误或不正确使用 API。例如,考虑先前描述的应用程序将文件名传递给FileReader构造函数。如果逻辑错误导致将null传递给构造函数,则构造函数将抛出NullPointerException。应用程序可以捕获此异常,但更有意义的是消除导致异常发生的错误。

运行时异常不受捕获或指定要求的约束。运行时异常是由RuntimeException及其子类指示的异常。

错误和运行时异常统称为未经检查的异常

绕过捕获或指定

一些程序员认为捕获或指定要求是异常机制中的一个严重缺陷,并通过在需要检查的异常位置使用未经检查的异常来绕过它。一般来说,这是不推荐的。章节未经检查的异常 - 争议讨论了何时适合使用未经检查的异常。

捕获和处理异常

原文:docs.oracle.com/javase/tutorial/essential/exceptions/handling.html

本节描述了如何使用三个异常处理组件——trycatchfinally块——编写异常处理程序。然后,解释了在 Java SE 7 中引入的try-with-resources 语句。try-with-resources 语句特别适用于使用Closeable资源的情况,比如流。

本节的最后部分通过一个示例演示并分析了各种情况下的发生情况。

以下示例定义并实现了一个名为ListOfNumbers的类。在构造时,ListOfNumbers创建一个包含 10 个顺序值为 0 到 9 的Integer元素的ArrayListListOfNumbers类还定义了一个名为writeList的方法,该方法将数字列表写入名为OutFile.txt的文本文件。此示例使用了java.io中定义的输出类,这些类在基本 I/O 中有介绍。

// Note: This class will not compile yet.
import java.io.*;
import java.util.List;
import java.util.ArrayList;

public class ListOfNumbers {

    private List<Integer> list;
    private static final int SIZE = 10;

    public ListOfNumbers () {
        list = new ArrayList<Integer>(SIZE);
        for (int i = 0; i < SIZE; i++) {
            list.add(new Integer(i));
        }
    }

    public void writeList() {
	// The FileWriter constructor throws IOException, which must be caught.
        PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));

        for (int i = 0; i < SIZE; i++) {
            // The get(int) method throws IndexOutOfBoundsException, which must be caught.
            out.println("Value at: " + i + " = " + list.get(i));
        }
        out.close();
    }
}

粗体的第一行是对构造函数的调用。构造函数在文件上初始化一个输出流。如果文件无法打开,构造函数会抛出一个IOException异常。第二行粗体是对ArrayList类的get方法的调用,如果其参数的值太小(小于 0)或太大(大于ArrayList当前包含的元素数量),则会抛出IndexOutOfBoundsException异常。

如果尝试编译ListOfNumbers类,编译器会打印有关FileWriter构造函数抛出的异常的错误消息。但是,它不会显示有关get方法抛出的异常的错误消息。原因是构造函数抛出的异常IOException是一个已检查异常,而get方法抛出的异常IndexOutOfBoundsException是一个未检查异常。

现在您熟悉了ListOfNumbers类以及其中可能抛出异常的位置,您可以编写异常处理程序来捕获和处理这些异常了。

try 块

原文:docs.oracle.com/javase/tutorial/essential/exceptions/try.html

构建异常处理程序的第一步是将可能会抛出异常的代码放在一个try块中。一般来说,一个try块看起来像下面这样:

try {
    *code*
}
*catch and finally blocks . . .*

在示例中标记为*code*的部分包含一个或多个可能会抛出异常的合法代码行。(catchfinally块将在接下来的两个小节中解释。)

要为ListOfNumbers类中的writeList方法构建一个异常处理程序,将writeList方法中可能会抛出异常的语句放在一个try块中。有多种方法可以做到这一点。你可以将每一行可能会抛出异常的代码放在自己的try块中,并为每个提供单独的异常处理程序。或者,你可以将所有的writeList代码放在一个单独的try块中,并为其关联多个处理程序。以下清单使用一个try块来处理整个方法,因为相关代码非常简短。

private List<Integer> list;
private static final int SIZE = 10;

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("Entered try statement");
        FileWriter f = new FileWriter("OutFile.txt");
        out = new PrintWriter(f);
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    }
    catch and finally blocks  . . .
}

如果在try块中发生异常,该异常将由与之关联的异常处理程序处理。要为try块关联一个异常处理程序,必须在其后放置一个catch块;下一节,catch 块,将向你展示如何做到这一点。

异常处理块

原文:docs.oracle.com/javase/tutorial/essential/exceptions/catch.html

通过在try块后直接提供一个或多个catch块,将异常处理程序与try块关联起来。在try块结束和第一个catch块开始之间不能有任何代码。

try {

} catch (*ExceptionType name*) {

} catch (*ExceptionType name*) {

}

每个catch块都是一个异常处理程序,处理其参数指示的异常类型。参数类型*ExceptionType*声明了处理程序可以处理的异常类型,必须是从Throwable类继承的类名。处理程序可以使用*name*引用异常。

catch块包含在异常处理程序被调用时执行的代码。当处理程序是调用堆栈中第一个*ExceptionType*与抛出异常类型匹配的处理程序时,运行时系统会调用异常处理程序。如果抛出的对象可以合法地分配给异常处理程序的参数,则系统认为它匹配。

以下是writeList方法的两个异常处理程序:

try {

} catch (IndexOutOfBoundsException e) {
    System.err.println("IndexOutOfBoundsException: " + e.getMessage());
} catch (IOException e) {
    System.err.println("Caught IOException: " + e.getMessage());
}

异常处理程序不仅可以打印错误消息或停止程序。它们可以进行错误恢复,提示用户做出决定,或者使用链式异常将错误传播到更高级别的处理程序,如链式异常部分所述。

使用一个异常处理程序捕获多种类型的异常

在 Java SE 7 及更高版本中,单个catch块可以处理多种类型的异常。这个特性可以减少代码重复,并减少捕获过于宽泛异常的诱惑。

catch子句中,指定阻止处理的异常类型,并用竖线(|)分隔每种异常类型:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

注意:如果一个catch块处理多种异常类型,则catch参数隐式为final。在这个例子中,catch参数exfinal,因此您不能在catch块内为其分配任何值。

finally 块

原文:docs.oracle.com/javase/tutorial/essential/exceptions/finally.html

finally总是try块退出时执行。这确保了即使发生意外异常,finally块也会被执行。但finally不仅仅用于异常处理,它允许程序员避免清理代码被returncontinuebreak意外绕过。将清理代码放在finally块中始终是一个良好的实践,即使不预期发生异常。


注意:如果 JVM 在执行trycatch代码时退出,则finally块可能不会执行。


你一直在这里工作的writeList方法的try块打开了一个PrintWriter。程序应该在退出writeList方法之前关闭该流。这带来了一个有点复杂的问题,因为writeListtry块可以以三种方式之一退出。

  1. new FileWriter语句失败并抛出IOException

  2. list.get(i)语句失败并抛出IndexOutOfBoundsException

  3. 一切顺利,try块正常退出。

无论try块内发生了什么,运行时系统始终执行finally块中的语句。因此,这是执行清理操作的完美位置。

以下writeList方法的finally块清理并关闭PrintWriterFileWriter

finally {
    if (out != null) { 
        System.out.println("Closing PrintWriter");
        out.close(); 
    } else { 
        System.out.println("PrintWriter not open");
    } 
    if (f != null) {
	    System.out.println("Closing FileWriter");
	    f.close();
	}	
} 


重要:在关闭文件或恢复资源时,请使用try-with-resources 语句而不是finally块。以下示例使用try-with-resources 语句清理和关闭writeList方法的PrintWriterFileWriter

public void writeList() throws IOException {
    try (FileWriter f = new FileWriter("OutFile.txt");
         PrintWriter out = new PrintWriter(f)) {
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    }
}

try-with-resources 语句在不再需要时自动释放系统资源。请参见 try-with-resources 语句。


try-with-resources 语句

原文:docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

try-with-resources 语句是一个声明一个或多个资源的try语句。资源是程序在使用完后必须关闭的对象。try-with-resources 语句确保每个资源在语句结束时关闭。任何实现java.lang.AutoCloseable接口的对象,包括所有实现java.io.Closeable接口的对象,都可以用作资源。

以下示例从文件中读取第一行。它使用FileReaderBufferedReader的实例从文件中读取数据。FileReaderBufferedReader是在程序使用完后必须关闭的资源:

	static String readFirstLineFromFile(String path) throws IOException {
	    try (FileReader fr = new FileReader(path);
	         BufferedReader br = new BufferedReader(fr)) {
	        return br.readLine();
	    }
	}	

在这个示例中,try-with-resources 语句中声明的资源是FileReaderBufferedReader。这些资源的声明语句出现在try关键字之后的括号内。在 Java SE 7 及更高版本中,FileReaderBufferedReader类实现了java.lang.AutoCloseable接口。因为FileReaderBufferedReader实例是在try-with-resources 语句中声明的,无论try语句是否正常完成或突然中断(因为BufferedReader.readLine方法抛出IOException),它们都将被关闭。

在 Java SE 7 之前,可以使用finally块来确保资源在try语句正常完成或突然中断时关闭。以下示例使用finally块而不是try-with-resources 语句:

static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {

    FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr);
    try {
        return br.readLine();
    } finally {
        br.close();
        fr.close();
    }
}

然而,这个示例可能会有资源泄漏。程序不能仅仅依赖垃圾回收器(GC)在完成后回收资源的内存。程序还必须将资源释放回操作系统,通常通过调用资源的close方法。但是,如果程序在 GC 回收资源之前未能执行此操作,则释放资源所需的信息将丢失。这个资源,仍然被操作系统视为正在使用,已经泄漏。

在这个示例中,如果readLine方法抛出异常,并且finally块中的br.close()语句也抛出异常,那么FileReader就会泄漏。因此,使用try-with-resources 语句而不是finally块来关闭程序的资源。

如果readLineclose方法都抛出异常,则readFirstLineFromFileWithFinallyBlock方法会抛出从finally块中抛出的异常;从try块中抛出的异常会被抑制。相比之下,在示例readFirstLineFromFile中,如果try块和try-with-resources 语句都抛出异常,则readFirstLineFromFile方法会抛出从try块中抛出的异常;从try-with-resources 块中抛出的异常会被抑制。在 Java SE 7 及更高版本中,您可以检索被抑制的异常;有关更多信息,请参阅被抑制的异常部分。

以下示例检索打包在 zip 文件zipFileName中的文件的名称,并创建一个包含这些文件名称的文本文件:

public static void writeToFileZipFileContents(String zipFileName,
                                           String outputFileName)
                                           throws java.io.IOException {

    java.nio.charset.Charset charset =
         java.nio.charset.StandardCharsets.US_ASCII;
    java.nio.file.Path outputFilePath =
         java.nio.file.Paths.get(outputFileName);

    // Open zip file and create output file with 
    // try-with-resources statement

    try (
        java.util.zip.ZipFile zf =
             new java.util.zip.ZipFile(zipFileName);
        java.io.BufferedWriter writer = 
            java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
    ) {
        // Enumerate each entry
        for (java.util.Enumeration entries =
                                zf.entries(); entries.hasMoreElements();) {
            // Get the entry name and write it to the output file
            String newLine = System.getProperty("line.separator");
            String zipEntryName =
                 ((java.util.zip.ZipEntry)entries.nextElement()).getName() +
                 newLine;
            writer.write(zipEntryName, 0, zipEntryName.length());
        }
    }
}

在此示例中,try-with-resources 语句包含两个声明,它们之间用分号分隔:ZipFileBufferedWriter。当直接跟在其后的代码块正常终止或因异常终止时,BufferedWriterZipFile对象的close方法会按照这个顺序自动调用。请注意,资源的close方法按照它们创建的相反顺序调用。

以下示例使用try-with-resources 语句自动关闭java.sql.Statement对象:

public static void viewTable(Connection con) throws SQLException {

    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";

    try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);

        while (rs.next()) {
            String coffeeName = rs.getString("COF_NAME");
            int supplierID = rs.getInt("SUP_ID");
            float price = rs.getFloat("PRICE");
            int sales = rs.getInt("SALES");
            int total = rs.getInt("TOTAL");

            System.out.println(coffeeName + ", " + supplierID + ", " + 
                               price + ", " + sales + ", " + total);
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
}

此示例中使用的资源java.sql.Statement是 JDBC 4.1 及更高版本 API 的一部分。

注意try-with-resources 语句可以像普通的try语句一样具有catchfinally块。在try-with-resources 语句中,任何catchfinally块都会在声明的资源关闭后运行。

被抑制的异常

try-with-resources 语句相关联的代码块中可能会抛出异常。在示例writeToFileZipFileContents中,异常可能会从try块中抛出,当尝试关闭ZipFileBufferedWriter对象时,try-with-resources 语句最多可能会抛出两个异常。如果从try块中抛出异常,并且从try-with-resources 语句中抛出一个或多个异常,则这些从try-with-resources 语句中抛出的异常会被抑制,而由该块抛出的异常就是writeToFileZipFileContents方法抛出的异常。您可以通过调用由try块抛出的异常的Throwable.getSuppressed方法来检索这些被抑制的异常。

实现 AutoCloseable 或 Closeable 接口的类

查看AutoCloseableCloseable接口的 Javadoc,以获取实现这两个接口之一的类列表。Closeable接口扩展了AutoCloseable接口。Closeable接口的close方法会抛出IOException类型的异常,而AutoCloseable接口的close方法会抛出Exception类型的异常。因此,AutoCloseable接口的子类可以重写close方法的行为,以抛出特定的异常,比如IOException,或者根本不抛出异常。

将所有内容整合在一起

原文:docs.oracle.com/javase/tutorial/essential/exceptions/putItTogether.html

前面的部分描述了如何为ListOfNumbers类中的writeList方法构造trycatchfinally代码块。现在,让我们走一遍代码,看看可能发生什么。

当所有组件放在一起时,writeList方法看起来像下面这样。

public void writeList() {
    PrintWriter out = null;

    try {
        System.out.println("Entering" + " try statement");

        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    } catch (IndexOutOfBoundsException e) {
        System.err.println("Caught IndexOutOfBoundsException: "
                           +  e.getMessage());

    } catch (IOException e) {
        System.err.println("Caught IOException: " +  e.getMessage());

    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } 
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

如前所述,此方法的try块有三种不同的退出可能性;以下是其中两种。

  1. try语句中的代码失败并抛出异常。这可能是由new FileWriter语句引起的IOException,也可能是由for循环中错误的索引值引起的IndexOutOfBoundsException

  2. 一切顺利,try语句正常退出。

让我们看看在这两种退出可能性中writeList方法中会发生什么。

情况 1:发生异常

创建FileWriter的语句可能因多种原因而失败。例如,如果程序无法创建或写入指定文件,则FileWriter的构造函数会抛出IOException

FileWriter抛出IOException时,运行时系统立即停止执行try块;正在执行的方法调用不会完成。然后,运行时系统从方法调用堆栈的顶部开始搜索适当的异常处理程序。在本例中,当发生IOException时,FileWriter构造函数位于调用堆栈的顶部。然而,FileWriter构造函数没有适当的异常处理程序,因此运行时系统检查下一个方法——writeList方法——在方法调用堆栈中。writeList方法有两个异常处理程序:一个用于IOException,一个用于IndexOutOfBoundsException

运行时系统按照try语句后出现的顺序检查writeList的处理程序。第一个异常处理程序的参数是IndexOutOfBoundsException。这与抛出的异常类型不匹配,因此运行时系统检查下一个异常处理程序——IOException。这与抛出的异常类型匹配,因此运行时系统结束了对适当异常处理程序的搜索。现在,运行时已找到适当的处理程序,将执行该catch块中的代码。

异常处理程序执行后,运行时系统将控制权传递给finally块。finally块中的代码无论上面捕获的异常如何,都会执行。在这种情况下,FileWriter从未被打开,也不需要关闭。finally块执行完毕后,程序将继续执行finally块后的第一条语句。

下面是当抛出IOException时出现的ListOfNumbers程序的完整输出。

Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open 

以下清单中的粗体代码显示了在这种情况下执行的语句:

public void writeList() {
   PrintWriter out = null;

    try {
        System.out.println("Entering try statement");
        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++)
            out.println("Value at: " + i + " = " + list.get(i));

    } catch (IndexOutOfBoundsException e) {
        System.err.println("Caught IndexOutOfBoundsException: "
                           + e.getMessage());

    } catch (IOException e) {
        System.err.println("Caught IOException: " + e.getMessage());
    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } 
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

情景 2:try 块正常退出

在这种情况下,try块范围内的所有语句都成功执行且没有抛出异常。执行流程跳出try块,运行时系统将控制权传递给finally块。因为一切顺利,当控制权到达finally块时,PrintWriter是打开状态,finally块关闭了PrintWriter。同样,在finally块执行完毕后,程序将继续执行finally块后的第一条语句。

这是ListOfNumbers程序在没有抛出异常时的输出。

Entering try statement
Closing PrintWriter

以下示例中的粗体代码显示了在这种情况下执行的语句。

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("Entering try statement");
        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++)
            out.println("Value at: " + i + " = " + list.get(i));

    } catch (IndexOutOfBoundsException e) {
        System.err.println("Caught IndexOutOfBoundsException: "
                           + e.getMessage());

    } catch (IOException e) {
        System.err.println("Caught IOException: " + e.getMessage());

    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } 
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

指定方法抛出的异常

原文:docs.oracle.com/javase/tutorial/essential/exceptions/declaring.html

前一节展示了如何为ListOfNumbers类中的writeList方法编写异常处理程序。有时,代码捕获可能发生的异常是合适的。然而,在其他情况下,最好让调用堆栈中更高层的方法处理异常。例如,如果你将ListOfNumbers类作为一个类包的一部分提供,你可能无法预料到包的所有用户的需求。在这种情况下,最好不要捕获异常,而是让调用堆栈中更高层的方法来处理它。

如果writeList方法不捕获其中可能发生的已检查异常,那么writeList方法必须指定它可以抛出这些异常。让我们修改原始的writeList方法,以指定它可以抛出的异常,而不是捕获它们。为了提醒你,这里是原始版本的writeList方法,它不会编译通过。

public void writeList() {
    PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
    for (int i = 0; i < SIZE; i++) {
        out.println("Value at: " + i + " = " + list.get(i));
    }
    out.close();
}

要指定writeList可能会抛出两个异常,需要在writeList方法的方法声明中添加一个throws子句。throws子句包括throws关键字,后面跟着一个逗号分隔的由该方法抛出的所有异常的列表。该子句放在方法名和参数列表之后,方法范围定义的大括号之前;以下是一个示例。

public void writeList() throws IOException, IndexOutOfBoundsException {

请记住IndexOutOfBoundsException是一个未经检查的异常;在throws子句中包含它并不是强制性的。你可以只写如下内容。

public void writeList() throws IOException {

如何抛出异常

原文:docs.oracle.com/javase/tutorial/essential/exceptions/throwing.html

在捕获异常之前,某个地方的代码必须抛出异常。任何代码都可以抛出异常:您的代码、其他人编写的包中的代码(例如 Java 平台提供的包)或 Java 运行时环境。无论是什么引发了异常,它总是使用 throw 语句抛出。

正如您可能已经注意到的,Java 平台提供了许多异常类。所有这些类都是 Throwable 类的后代,所有这些类都允许程序在程序执行期间区分各种可能发生的异常类型。

您还可以创建自己的异常类来表示您编写的类中可能发生的问题。实际上,如果您是一个包开发者,您可能需要创建自己的一组异常类,以允许用户区分在您的包中可能发生的错误与在 Java 平台或其他包中发生的错误。

您还可以创建链接异常。有关更多信息,请参阅链接异常部分。

抛出语句

所有方法都使用 throw 语句来抛出异常。throw 语句需要一个参数:一个可抛出对象。可抛出对象是 Throwable 类的任何子类的实例。这里是一个 throw 语句的示例。

throw *someThrowableObject*;

让我们看看 throw 语句的上下文。以下 pop 方法取自实现常见堆栈对象的类。该方法从堆栈中移除顶部元素并返回该对象。

public Object pop() {
    Object obj;

    if (size == 0) {
        throw new EmptyStackException();
    }

    obj = objectAt(size - 1);
    setObjectAt(size - 1, null);
    size--;
    return obj;
}

pop 方法检查堆栈上是否有任何元素。如果堆栈为空(其大小等于 0),pop 实例化一个新的 EmptyStackException 对象(java.util 的成员)并将其抛出。本章的创建异常类部分解释了如何创建自己的异常类。现在,您只需要记住您只能抛出继承自 java.lang.Throwable 类的对象。

注意,pop 方法的声明中不包含 throws 子句。EmptyStackException 不是一个受检异常,因此 pop 不需要声明它可能发生。

Throwable 类及其子类

继承自Throwable类的对象包括直接后代(直接从Throwable类继承的对象)和间接后代(从Throwable类的子类或孙子类继承的对象)。下图说明了Throwable类及其最重要的子类的类层次结构。正如你所看到的,Throwable有两个直接后代:ErrorException

Throwable 类及其最重要的子类。

Throwable 类。

错误类

当 Java 虚拟机发生动态链接失败或其他严重故障时,虚拟机会抛出一个Error。简单的程序通常不会捕获或抛出Error

异常类

大多数程序会抛出和捕获从Exception类派生的对象。Exception表示发生了问题,但不是严重的系统问题。你编写的大多数程序会抛出和捕获Exception,而不是Error

Java 平台定义了Exception类的许多后代。这些后代表示可能发生的各种异常类型。例如,IllegalAccessException表示找不到特定方法,而NegativeArraySizeException表示程序尝试创建一个负大小的数组。

一个Exception子类,RuntimeException,用于指示 API 的不正确使用的异常。一个运行时异常的例子是NullPointerException,当一个方法尝试通过null引用访问对象的成员时会发生。本节未经检查的异常 — 争议讨论了为什么大多数应用程序不应该抛出运行时异常或子类化RuntimeException

链式异常

原文:docs.oracle.com/javase/tutorial/essential/exceptions/chained.html

应用程序通常通过抛出另一个异常来响应异常。实际上,第一个异常导致第二个异常。知道一个异常导致另一个异常时会非常有帮助。链式异常帮助程序员做到这一点。

以下是支持链式异常的Throwable中的方法和构造函数。

Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)

initCauseThrowable构造函数的Throwable参数是导致当前异常的异常。getCause返回导致当前异常的异常,initCause设置当前异常的原因。

以下示例展示了如何使用链式异常。

try {

} catch (IOException e) {
    throw new SampleException("Other IOException", e);
}

在这个例子中,当捕获到IOException时,会创建一个带有原始原因附加的新的SampleException异常,并将异常链抛到下一个更高级别的异常处理程序。

访问堆栈跟踪信息

现在假设更高级别的异常处理程序想要以自己的格式转储堆栈跟踪。


定义: 堆栈跟踪提供了关于当前线程执行历史的信息,并列出了在异常发生时调用的类和方法的名称。堆栈跟踪是一个有用的调试工具,当抛出异常时,通常会利用它。


以下代码展示了如何在异常对象上调用getStackTrace方法。

catch (Exception cause) {
    StackTraceElement elements[] = cause.getStackTrace();
    for (int i = 0, n = elements.length; i < n; i++) {       
        System.err.println(elements[i].getFileName()
            + ":" + elements[i].getLineNumber() 
            + ">> "
            + elements[i].getMethodName() + "()");
    }
}

日志记录 API

下一个代码片段记录了异常发生的位置,位于catch块内部。然而,它不是手动解析堆栈跟踪并将输出发送到System.err(),而是使用java.util.logging包中的日志记录功能将输出发送到文件。

try {
    Handler handler = new FileHandler("OutFile.log");
    Logger.getLogger("").addHandler(handler);

} catch (IOException e) {
    Logger logger = Logger.getLogger("*package.name*"); 
    StackTraceElement elements[] = e.getStackTrace();
    for (int i = 0, n = elements.length; i < n; i++) {
        logger.log(Level.WARNING, elements[i].getMethodName());
    }
}

创建异常类

原文:docs.oracle.com/javase/tutorial/essential/exceptions/creating.html

在选择要抛出的异常类型时,你可以使用其他人编写的异常 — Java 平台提供了许多可以使用的异常类 — 或者你可以编写自己的异常。如果对以下任何问题回答是肯定的,那么你应该编写自己的异常类;否则,你可能可以使用别人的。

  • 是否需要一个 Java 平台中没有的异常类型?

  • 如果用户能够区分你的异常和其他供应商编写的异常,这是否有助于用户?

  • 你的代码是否抛出了多个相关的异常?

  • 如果使用别人的异常,用户是否能够访问这些异常?一个类似的问题是,你的包是否独立且自包含?

一个示例

假设你正在编写一个链表类。该类支持以下方法,以及其他方法:

  • objectAt(int n) — 返回列表中第n个位置的对象。如果参数小于 0 或大于当前列表中对象的数量,则抛出异常。

  • firstObject() — 返回列表中的第一个对象。如果列表不包含对象,则抛出异常。

  • indexOf(Object o) — 搜索列表中指定的Object并返回其在列表中的位置。如果传入方法的对象不在列表中,则抛出异常。

链表类可以抛出多个异常,能够使用一个异常处理程序捕获链表抛出的所有异常将会很方便。此外,如果计划在一个包中分发你的链表,所有相关代码应该打包在一起。因此,链表应该提供自己的一组异常类。

下图展示了链表抛出的异常可能的类层次结构。

链表抛出的异常可能的类层次结构。

示例异常类层次结构。

选择一个超类

任何Exception子类都可以用作LinkedListException的父类。然而,快速浏览这些子类显示它们不合适,因为它们要么过于专业化,要么与LinkedListException完全无关。因此,LinkedListException的父类应该是Exception

你编写的大多数小程序和应用程序将抛出Exception对象。Error通常用于系统中的严重、严重错误,例如阻止 JVM 运行的错误。


注意: 为了编写可读性强的代码,将Exception字符串附加到所有直接或间接继承自Exception类的类名后是一个好习惯。


未检查异常 — 争议

原文:docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html

因为 Java 编程语言不要求方法捕获或指定未检查异常(RuntimeExceptionError及其子类),程序员可能会倾向于编写仅抛出未检查异常的代码,或者使所有异常子类继承自RuntimeException。这两种捷径使程序员能够编写代码,而不必理会编译器错误,也不必指定或捕获任何异常。尽管这对程序员来说可能很方便,但它绕过了catchspecify要求的意图,可能会给使用您的类的其他人造成问题。

设计者为什么决定强制一种方法来指定在其范围内可能抛出的所有未捕获的已检查异常?方法可能抛出的任何Exception都是方法的公共编程接口的一部分。调用方法的人必须了解方法可能抛出的异常,以便他们可以决定如何处理这些异常。这些异常与方法的编程接口一样重要,就像它的参数和return值一样。

下一个问题可能是:“如果记录方法的 API,包括它可能抛出的异常是如此重要,为什么不也指定运行时异常呢?”运行时异常代表的是由编程问题导致的问题,因此,API 客户端代码不能合理地预期从中恢复或以任何方式处理它们。这些问题包括算术异常,例如除以零;指针异常,例如尝试通过空引用访问对象;以及索引异常,例如尝试通过太大或太小的索引访问数组元素。

运行时异常可能在程序的任何地方发生,在典型情况下可能非常多。在每个方法声明中添加运行时异常会降低程序的清晰度。因此,编译器不要求您捕获或指定运行时异常(尽管您可以)。

一个常见的情况是抛出RuntimeException的情况是当用户错误调用方法时。例如,一个方法可以检查其参数是否不正确为null。如果参数为null,方法可能会抛出NullPointerException,这是一个未检查异常。

一般来说,不要仅仅因为不想麻烦指定方法可能抛出的异常而抛出RuntimeException或创建RuntimeException的子类。

这里是底线指导原则:如果客户端可以合理地预期从异常中恢复,那么将其作为已检查异常。如果客户端无法从异常中恢复,那么将其作为未检查异常。

异常的优点

原文:docs.oracle.com/javase/tutorial/essential/exceptions/advantages.html

现在您知道什么是异常以及如何使用它们,是时候学习在程序中使用异常的优点了。

优点 1:将错误处理代码与“常规”代码分开

异常提供了一种将发生异常情况时的处理细节与程序的主要逻辑分开的手段。在传统编程中,错误检测、报告和处理经常导致令人困惑的意大利面代码。例如,考虑这里的伪代码方法,它将整个文件读入内存。

readFile {
    *open the file;
    determine its size;
    allocate that much memory;
    read the file into memory;
    close the file;*
}

乍一看,这个函数似乎足够简单,但它忽略了所有以下潜在的错误。

  • 如果文件无法打开会发生什么?

  • 如果无法确定文件的长度会发生什么?

  • 如果无法分配足够的内存会发生什么?

  • 如果读取失败会发生什么?

  • 如果文件无法关闭会发生什么?

要处理这种情况,readFile 函数必须有更多的代码来进行错误检测、报告和处理。以下是该函数可能的示例。

errorCodeType readFile {
    initialize errorCode = 0;

    *open the file;*
    if (*theFileIsOpen*) {
        *determine the length of the file;*
        if (*gotTheFileLength*) {
            *allocate that much memory;*
            if (*gotEnoughMemory*) {
                *read the file into memory;*
                if (*readFailed*) {
                    errorCode = -1;
                }
            } else {
                errorCode = -2;
            }
        } else {
            errorCode = -3;
        }
        *close the file;*
        if (*theFileDidntClose* && *errorCode* == 0) {
            errorCode = -4;
        } else {
            errorCode = errorCode and -4;
        }
    } else {
        errorCode = -5;
    }
    return errorCode;
}

这里有太多的错误检测、报告和返回,原始的七行代码在混乱中丢失了。更糟糕的是,代码的逻辑流也丢失了,因此很难判断代码是否在做正确的事情:如果函数无法分配足够的内存,文件是否真的被关闭了?当您在编写三个月后修改方法时,确保代码继续执行正确的事情更加困难。许多程序员通过简单地忽略它来解决这个问题——当他们的程序崩溃时会报告错误。

异常使您能够编写代码的主要流程,并在其他地方处理异常情况。如果readFile函数使用异常而不是传统的错误管理技术,它会看起来更像以下内容。

*readFile* {
    try {
        *open the file;
        determine its size;
        allocate that much memory;
        read the file into memory;
        close the file;*
    } catch (*fileOpenFailed*) {
       *doSomething;*
    } catch (*sizeDeterminationFailed*) {
        *doSomething;*
    } catch (*memoryAllocationFailed*) {
        *doSomething;*
    } catch (*readFailed*) {
        *doSomething;*
    } catch (*fileCloseFailed*) {
        *doSomething;*
    }
}

请注意,异常并不免除您进行错误检测、报告和处理的工作,但它们确实帮助您更有效地组织工作。

优点 2:将错误传播到调用堆栈上

异常的第二个优点是能够将错误报告传播到方法的调用堆栈上。假设readFile方法是主程序进行的一系列嵌套方法调用中的第四个方法:method1调用method2method2调用method3,最终调用readFile

method1 {
    *call method2;*
}

method2 {
    *call method3;*
}

method3 {
    *call readFile;*
}

假设method1是唯一对readFile中可能发生的错误感兴趣的方法。传统的错误通知技术会强制method2method3readFile返回的错误代码传播到调用堆栈上,直到错误代码最终到达method1——唯一对它们感兴趣的方法。

method1 {
    errorCodeType error;
    error = *call method2;*
    if (error)
        *doErrorProcessing;*
    else
        *proceed;*
}

errorCodeType method2 {
    errorCodeType error;
    error = call method3;
    if (error)
        return error;
    else
        *proceed;*
}

errorCodeType method3 {
    errorCodeType error;
    error = call readFile;
    if (error)
        return error;
    else
        *proceed;*
}

请记住,Java 运行时环境会逆向搜索调用堆栈,以找到任何对处理特定异常感兴趣的方法。一个方法可以规避其内部抛出的任何异常,从而允许调用堆栈中更高层的方法捕获它。因此,只有关心错误的方法才需要担心检测错误。

method1 {
    try {
        *call method2;*
    } catch (*exception* e) {
        *doErrorProcessing;*
    }
}

method2 throws *exception* {
    *call method3;*
}

method3 throws exception {
    *call readFile;*
}

然而,正如伪代码所示,规避异常需要中间方法付出一些努力。在方法内部可能抛出的任何已检查异常必须在其throws子句中指定。

优势 3:分组和区分错误类型

因为程序中抛出的所有异常都是对象,异常的分组或分类是类层次结构的自然结果。Java 平台中一组相关的异常类的示例是java.io中定义的那些 — IOException及其后代。IOException是最一般的,表示在执行 I/O 时可能发生的任何类型的错误。其后代表示更具体的错误。例如,FileNotFoundException表示无法在磁盘上找到文件。

一个方法可以编写特定的处理程序,可以处理非常具体的异常。FileNotFoundException类没有后代,因此以下处理程序只能处理一种类型的异常。

catch (FileNotFoundException e) {
    ...
}

一个方法可以通过在catch语句中指定任何异常的超类来基于其组或一般类型捕获异常。例如,为了捕获所有 I/O 异常,无论其具体类型如何,异常处理程序指定一个IOException参数。

catch (IOException e) {
    ...
}

这个处理程序将能够捕获所有 I/O 异常,包括FileNotFoundExceptionEOFException等。您可以通过查询传递给异常处理程序的参数来找到发生的详细信息。例如,使用以下内容打印堆栈跟踪。

catch (IOException e) {
    // Output goes to System.err.
    e.printStackTrace();
    // Send trace to stdout.
    e.printStackTrace(System.out);
}

你甚至可以设置一个异常处理程序,用于处理这里的任何Exception

// *A (too) general exception handler*
catch (Exception e) {
    ...
}

Exception类接近Throwable类层次结构的顶部。因此,此处理程序将捕获许多其他异常,除了处理程序打算捕获的异常之外。如果您只希望程序执行的操作是打印出用户的错误消息,然后退出,您可能希望以这种方式处理异常。

然而,在大多数情况下,您希望异常处理程序尽可能具体。原因是处理程序必须首先确定发生了什么类型的异常,然后才能决定最佳的恢复策略。实际上,通过不捕获特定错误,处理程序必须适应任何可能性。过于一般化的异常处理程序可能会使代码更容易出错,因为它会捕获和处理程序员未预料到的异常,而处理程序并不打算处理这些异常。

正如所指出的,您可以创建异常组并以一般方式处理异常,或者您可以使用特定的异常类型来区分异常并以精确方式处理异常。

摘要

原文:docs.oracle.com/javase/tutorial/essential/exceptions/summary.html

程序可以使用异常来指示发生了错误。要抛出异常,请使用throw语句,并提供一个异常对象 — 一个Throwable的后代 — 以提供有关发生的具体错误的信息。抛出未捕获的已检查异常的方法必须在其声明中包含一个throws子句。

程序可以通过使用trycatchfinally块的组合来捕获异常。

  • try块标识出可能发生异常的代码块。

  • catch块标识出一个代码块,称为异常处理程序,可以处理特定类型的异常。

  • finally块标识出保证执行的代码块,并且是关闭文件、恢复资源以及在try块中封闭的代码之后进行清理的正确位置。

try语句应至少包含一个catch块或一个finally块,并且可以有多个catch块。

异常对象的类表示抛出的异常类型。异常对象可以包含有关错误的进一步信息,包括错误消息。通过异常链接,一个异常可以指向导致它的异常,后者又可以指向导致的异常,依此类推。

问题和练习

原文:docs.oracle.com/javase/tutorial/essential/exceptions/QandE/questions.html

问题

  1. 以下代码是否合法?

    try {
    
    } finally {
    
    }
    
    
  2. 以下处理程序可以捕获哪些异常类型?

    catch (Exception e) {
    
    }
    
    

    使用这种类型的异常处理程序有什么问题?

  3. 以下异常处理程序的写法有什么问题?这段代码能编译吗?

    try {
    
    } catch (Exception e) {
    
    } catch (ArithmeticException a) {
    
    }
    
    
  4. 将第一个列表中的每种情况与第二个列表中的一项进行匹配。

    1. `int[] A;

      A[0] = 0;`

    2. JVM 开始运行您的程序,但 JVM 找不到 Java 平台类。(Java 平台类位于classes.ziprt.jar中。)

    3. 一个程序正在读取流并达到流结束标记。

    4. 在关闭流之前和达到流结束标记之后,一个程序尝试再次读取流。

    5. __ 错误

    6. __ 已检查异常

    7. __ 编译错误

    8. __ 无例外

    练习

    1. ListOfNumbers.java中添加一个readList方法。该方法应从文件中读取int值,打印每个值,并将它们附加到向量的末尾。您应该捕获所有适当的错误。您还需要一个包含要读取的数字的文本文件。

    2. 修改以下cat方法以便能够编译。

      public static void cat(File file) {
          RandomAccessFile input = null;
          String line = null;
      
          try {
              input = new RandomAccessFile(file, "r");
              while ((line = input.readLine()) != null) {
                  System.out.println(line);
              }
              return;
          } finally {
              if (input != null) {
                  input.close();
              }
          }
      }
      
      

      检查您的答案。

课程:基本 I/O

原文:docs.oracle.com/javase/tutorial/essential/io/index.html

本课程涵盖了用于基本 I/O 的 Java 平台类。它首先关注* I/O 流*,这是一个极大简化 I/O 操作的强大概念。该课程还涉及序列化,它允许程序将整个对象写入流并再次读取它们。然后课程将查看文件 I/O 和文件系统操作,包括随机访问文件。

大多数在“ I/O 流”部分涵盖的类位于java.io包中。大多数在“文件 I/O”部分涵盖的类位于java.nio.file包中。

I/O 流

  • 字节流处理原始二进制数据的 I/O。

  • 字符流处理字符数据的 I/O,自动处理与本地字符集之间的转换。

  • 缓冲流通过减少对本机 API 的调用次数来优化输入和输出。

  • 扫描和格式化允许程序读取和写入格式化文本。

  • 从命令行进行 I/O 描述了标准流和控制台对象。

  • 数据流处理基本数据类型和String值的二进制 I/O。

  • 对象流处理对象的二进制 I/O。

文件 I/O(使用 NIO.2)

  • 什么是路径?探讨了文件系统上路径的概念。

  • Path 类介绍了java.nio.file包的基石类。

  • 路径操作查看了Path类中处理语法操作的方法。

  • 文件操作介绍了许多文件 I/O 方法共有的概念。

  • 检查文件或目录展示了如何检查文件的存在性和可访问性级别。

  • 删除文件或目录。

  • 复制文件或目录。

  • 移动文件或目录。

  • 管理元数据解释了如何读取和设置文件属性。

  • 读取、写入和创建文件展示了读取和写入文件的流和通道方法。

  • 随机访问文件展示了如何以非顺序方式读取或写入文件。

  • 创建和读取目录涵盖了特定于目录的 API,例如如何列出目录的内容。

  • 链接,符号或其他涵盖了与符号链接和硬链接相关的特定问题。

  • 遍历文件树演示了如何递归访问文件树中的每个文件和目录。

  • 查找文件展示了如何使用模式匹配搜索文件。

  • 监视目录变化展示了如何使用监视服务检测一个或多个目录中添加、删除或更新的文件。

  • 其他有用的方法涵盖了在本课程中其他地方无法涵盖的重要 API。

  • 旧版文件 I/O 代码展示了如何利用Path功能,如果你的旧代码使用了java.io.File类。提供了一个将java.io.File API 映射到java.nio.file API 的表格。

总结

本教程涵盖的关键要点总结。

问题和练习

通过尝试这些问题和练习来测试你在本教程中学到的知识。

I/O 类的实际运用

下一个教程中的许多示例,自定义网络,使用了本课程中描述的 I/O 流来从网络连接读取和写入。


安全注意事项: 一些 I/O 操作需要当前安全管理器的批准。这些教程中包含的示例程序是独立应用程序,默认情况下没有安全管理器。要在小程序中运行,大多数这些示例都需要进行修改。查看小程序的能力和限制以获取有关小程序所受的安全限制的信息。


I/O 流

原文:docs.oracle.com/javase/tutorial/essential/io/streams.html

I/O 流表示输入源或输出目的地。流可以表示许多不同类型的源和目的地,包括磁盘文件、设备、其他程序和内存数组。

流支持许多不同类型的数据,包括简单的字节、基本数据类型、本地化字符和对象。一些流只是传递数据;另一些以有用的方式操作和转换数据。

无论它们内部如何工作,所有流对使用它们的程序呈现相同简单的模型:流是一系列数据。程序使用输入流从源读取数据,一次读取一个项目:

将信息读入程序。

将信息读入程序。

程序使用输出流向目的地写入数据,一次写入一个项目:

将信息从程序写入。

将信息从程序写入。

在本课程中,我们将看到可以处理从基本值到高级对象的各种数据的流。

上图中的数据源和数据目的地可以是任何保存、生成或消耗数据的东西。显然,这包括磁盘文件,但源或目的地也可以是另一个程序、外围设备、网络套接字或数组。

在下一节中,我们将使用最基本的流类型,字节流,来演示流 I/O 的常见操作。作为示例输入,我们将使用示例文件xanadu.txt,其中包含以下诗句:

In Xanadu did Kubla Khan
A stately pleasure-dome decree:
Where Alph, the sacred river, ran
Through caverns measureless to man
Down to a sunless sea.

字节流

原文:docs.oracle.com/javase/tutorial/essential/io/bytestreams.html

程序使用字节流来执行 8 位字节的输入和输出。所有字节流类都是从InputStreamOutputStream继承而来。

有许多字节流类。为了演示字节流的工作原理,我们将重点放在文件 I/O 字节流FileInputStreamFileOutputStream上。其他类型的字节流使用方式基本相同;它们主要在构造方式上有所不同。

使用字节流

我们将通过检查一个名为CopyBytes的示例程序来探讨FileInputStreamFileOutputStream,该程序使用字节流逐字节复制xanadu.txt

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
    public static void main(String[] args) throws IOException {

        FileInputStream in = null;
        FileOutputStream out = null;

        try {
            in = new FileInputStream("xanadu.txt");
            out = new FileOutputStream("outagain.txt");
            int c;

            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

CopyBytes 在一个简单的循环中花费大部分时间,逐字节读取输入流并写入输出流,如下图所示。

简单的字节流输入和输出。

简单的字节流输入和输出。

总是关闭流

当不再需要流时关闭流非常重要—非常重要,以至于CopyBytes 使用finally块来确保即使发生错误,两个流也将被关闭。这种做法有助于避免严重的资源泄漏。

一个可能的错误是CopyBytes 无法打开一个或两个文件。当发生这种情况时,对应于文件的流变量从未从其初始的null值更改。这就是为什么CopyBytes 确保每个流变量在调用close之前包含一个对象引用。

何时不使用字节流

CopyBytes 看起来像一个普通程序,但实际上代表了一种应该避免的低级 I/O。由于xanadu.txt包含字符数据,最好的方法是使用字符流,如下一节所讨论的。还有用于更复杂数据类型的流。字节流应该仅用于最基本的 I/O。

那么为什么要谈论字节流呢?因为所有其他流类型都是建立在字节流之上的。

字符流

原文:docs.oracle.com/javase/tutorial/essential/io/charstreams.html

Java 平台使用 Unicode 约定存储字符值。字符流 I/O 会自动将内部格式与本地字符集进行转换。在西方区域,本地字符集通常是 ASCII 的 8 位超集。

对于大多数应用程序,使用字符流进行 I/O 与使用字节流进行 I/O 并无太大区别。使用流类进行的输入和输出会自动转换为本地字符集。使用字符流而不是字节流的程序会自动适应本地字符集,并且为国际化做好准备,而无需程序员额外努力。

如果国际化不是首要任务,您可以简单地使用字符流类,而不必过多关注字符集问题。稍后,如果国际化成为首要任务,您的程序可以在不进行大量重编码的情况下进行调整。查看国际化教程以获取更多信息。

使用字符流

所有字符流类都是从ReaderWriter继承而来。与字节流一样,有专门用于文件 I/O 的字符流类:FileReaderFileWriterCopyCharacters示例演示了这些类。

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopyCharacters {
    public static void main(String[] args) throws IOException {

        FileReader inputStream = null;
        FileWriter outputStream = null;

        try {
            inputStream = new FileReader("xanadu.txt");
            outputStream = new FileWriter("characteroutput.txt");

            int c;
            while ((c = inputStream.read()) != -1) {
                outputStream.write(c);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

CopyCharactersCopyBytes非常相似。最重要的区别在于,CopyCharacters使用FileReaderFileWriter进行输入和输出,而不是FileInputStreamFileOutputStream。请注意,CopyBytesCopyCharacters都使用一个int变量进行读取和写入。但是,在CopyCharacters中,int变量在其最后 16 位中保存字符值;而在CopyBytes中,int变量在其最后 8 位中保存byte值。

使用字节流的字符流

字符流通常是字节流的“包装器”。字符流使用字节流执行物理 I/O,而字符流处理字符和字节之间的转换。例如,FileReader使用FileInputStream,而FileWriter使用FileOutputStream

有两个通用的字节到字符的“桥梁”流:InputStreamReaderOutputStreamWriter。当没有符合您需求的预打包字符流类时,请使用它们来创建字符流。网络教程中的套接字课程展示了如何从套接字类提供的字节流创建字符流。

面向行的 I/O

字符 I/O 通常以比单个字符更大的单位进行。一个常见的单位是行:一串带有行终止符的字符。行终止符可以是回车/换行序列("\r\n"),单个回车("\r")或单个换行("\n")。支持所有可能的行终止符允许程序读取在任何广泛使用的操作系统上创建的文本文件。

让我们修改CopyCharacters示例以使用面向行的 I/O。为此,我们必须使用两个以前未见过的类,BufferedReaderPrintWriter。我们将在缓冲 I/O 和格式化中更深入地探讨这些类。现在,我们只关注它们对面向行的 I/O 的支持。

CopyLines示例调用BufferedReader.readLinePrintWriter.println来逐行进行输入和输出。

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;

public class CopyLines {
    public static void main(String[] args) throws IOException {

        BufferedReader inputStream = null;
        PrintWriter outputStream = null;

        try {
            inputStream = new BufferedReader(new FileReader("xanadu.txt"));
            outputStream = new PrintWriter(new FileWriter("characteroutput.txt"));

            String l;
            while ((l = inputStream.readLine()) != null) {
                outputStream.println(l);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

调用readLine返回带有行的文本。CopyLines使用println输出每一行,该方法会附加当前操作系统的行终止符。这可能与输入文件中使用的行终止符不同。

除了字符和行之外,还有许多结构化文本输入和输出的方式。更多信息,请参见扫描和格式化。

缓冲流

原文:docs.oracle.com/javase/tutorial/essential/io/buffers.html

到目前为止,我们看到的大多数示例都使用非缓冲的 I/O。这意味着每个读取或写入请求都直接由底层操作系统处理。这可能会使程序效率大大降低,因为每个这样的请求通常会触发磁盘访问、网络活动或其他相对昂贵的操作。

为了减少这种开销,Java 平台实现了缓冲 I/O 流。缓冲输入流从称为缓冲区的内存区域读取数据;只有当缓冲区为空时才调用本机输入 API。类似地,缓冲输出流将数据写入缓冲区,只有当缓冲区满时才调用本机输出 API。

一个程序可以使用我们已经多次使用的包装习惯将非缓冲流转换为缓冲流,其中非缓冲流对象传递给缓冲流类的构造函数。以下是您可能如何修改CopyCharacters示例中的构造函数调用以使用缓冲 I/O 的方式:

inputStream = new BufferedReader(new FileReader("xanadu.txt"));
outputStream = new BufferedWriter(new FileWriter("characteroutput.txt"));

有四个缓冲流类用于包装非缓冲流:BufferedInputStreamBufferedOutputStream创建缓冲字节流,而BufferedReaderBufferedWriter创建缓冲字符流。

刷新缓冲流

在关键点写出缓冲区而不等待其填满通常是有意义的。这被称为刷新缓冲区。

一些缓冲输出类支持自动刷新,可以通过可选的构造函数参数指定。启用自动刷新时,某些关键事件会导致缓冲区被刷新。例如,一个自动刷新的PrintWriter对象会在每次调用printlnformat时刷新缓冲区。查看格式化以获取更多关于这些方法的信息。

要手动刷新流,请调用其flush方法。flush方法对任何输出流都有效,但除非流是缓冲的,否则不会产生任何效果。

扫描和格式化

原文:docs.oracle.com/javase/tutorial/essential/io/scanfor.html

编程 I/O 通常涉及将数据翻译成人类喜欢处理的整洁格式。为了帮助您完成这些任务,Java 平台提供了两个 API。扫描器 API 将输入分解为与数据位相关联的单个标记。格式化 API 将数据组装成格式整齐、易读的形式。

扫描

原文:docs.oracle.com/javase/tutorial/essential/io/scanning.html

类型为Scanner的对象对于将格式化输入拆分为标记并根据其数据类型翻译单个标记非常有用。

将输入分解为标记

默认情况下,扫描器使用空白字符来分隔标记。(空白字符包括空格、制表符和行终止符。有关完整列表,请参考Character.isWhitespace的文档。)为了了解扫描的工作原理,让我们看看ScanXan,一个程序,它读取xanadu.txt中的单词并将它们逐行打印出来。

import java.io.*;
import java.util.Scanner;

public class ScanXan {
    public static void main(String[] args) throws IOException {

        Scanner s = null;

        try {
            s = new Scanner(new BufferedReader(new FileReader("xanadu.txt")));

            while (s.hasNext()) {
                System.out.println(s.next());
            }
        } finally {
            if (s != null) {
                s.close();
            }
        }
    }
}

请注意,当ScanXan完成对扫描器对象的操作时,会调用Scannerclose方法。即使扫描器不是一个流,你也需要关闭它以表示你已经完成了对其底层流的操作。

ScanXan的输出如下所示:

In
Xanadu
did
Kubla
Khan
A
stately
pleasure-dome
...

要使用不同的标记分隔符,调用useDelimiter(),指定一个正则表达式。例如,假设您希望标记分隔符是逗号,后面可以跟随空白。您可以调用,

s.useDelimiter(",\\s*");

翻译单个标记

ScanXan示例将所有输入标记视为简单的String值。Scanner还支持所有 Java 语言的基本类型(除了char),以及BigIntegerBigDecimal。此外,数值可以使用千位分隔符。因此,在US区域设置中,Scanner可以正确地将字符串"32,767"读取为整数值。

我们必须提及区域设置,因为千位分隔符和小数符是与区域设置相关的。因此,如果我们没有指定扫描器应该使用US区域设置,下面的示例在所有区域设置中都不会正确工作。这通常不是您需要担心的事情,因为您的输入数据通常来自与您相同区域设置的源。但是,这个示例是 Java 教程的一部分,会分发到世界各地。

ScanSum示例读取一组double值并将它们相加。以下是源代码:

import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Scanner;
import java.util.Locale;

public class ScanSum {
    public static void main(String[] args) throws IOException {

        Scanner s = null;
        double sum = 0;

        try {
            s = new Scanner(new BufferedReader(new FileReader("usnumbers.txt")));
            s.useLocale(Locale.US);

            while (s.hasNext()) {
                if (s.hasNextDouble()) {
                    sum += s.nextDouble();
                } else {
                    s.next();
                }   
            }
        } finally {
            s.close();
        }

        System.out.println(sum);
    }
}

这是示例输入文件,usnumbers.txt

8.5
32,767
3.14159
1,000,000.1

输出字符串为"1032778.74159"。在某些区域设置中,句号可能是不同的字符,因为System.out是一个PrintStream对象,该类不提供覆盖默认区域设置的方法。我们可以为整个程序覆盖区域设置,或者我们可以使用格式化,如下一主题中所述,格式化。

格式化

原文:docs.oracle.com/javase/tutorial/essential/io/formatting.html

实现格式化的流对象是PrintWriter(字符流类)或PrintStream(字节流类)的实例。


注意:你可能需要的唯一PrintStream对象是System.outSystem.err。(有关这些对象的更多信息,请参阅从命令行进行 I/O。)当您需要创建格式化输出流时,请实例化PrintWriter,而不是PrintStream


像所有字节和字符流对象一样,PrintStreamPrintWriter的实例实现了一组用于简单字节和字符输出的标准write方法。此外,PrintStreamPrintWriter都实现了相同的一组方法,用于将内部数据转换为格式化输出。提供了两个级别的格式化:

  • printprintln以标准方式格式化单个值。

  • format根据格式字符串几乎可以格式化任意数量的值,具有许多精确格式化选项。

printprintln方法

调用printprintln在使用适当的toString方法转换值后输出单个值。我们可以在Root示例中看到这一点:

public class Root {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);

        System.out.print("The square root of ");
        System.out.print(i);
        System.out.print(" is ");
        System.out.print(r);
        System.out.println(".");

        i = 5;
        r = Math.sqrt(i);
        System.out.println("The square root of " + i + " is " + r + ".");
    }
}

这是Root的输出:

The square root of 2 is 1.4142135623730951.
The square root of 5 is 2.23606797749979.

ir变量被格式化两次:第一次使用print重载中的代码,第二次是由 Java 编译器自动生成的转换代码,也利用了toString。您可以以这种方式格式化任何值,但对结果的控制不多。

format方法

format方法根据格式字符串格式化多个参数。格式字符串由静态文本与格式说明符嵌入在一起组成;除了格式说明符外,格式字符串不会改变输出。

格式字符串支持许多功能。在本教程中,我们只涵盖了一些基础知识。有关完整描述,请参阅 API 规范中的格式字符串语法

Root2示例使用单个format调用格式化两个值:

public class Root2 {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);

        System.out.format("The square root of %d is %f.%n", i, r);
    }
}

这里是输出:

The square root of 2 is 1.414214.

像这个示例中使用的三个一样,所有格式说明符都以%开头,并以指定正在生成的格式化输出类型的 1 个或 2 个字符转换结尾。这里使用的三个转换是:

  • d将整数值格式化为十进制值。

  • f将浮点值格式化为十进制值。

  • n输出特定于平台的换行符。

这里有一些其他转换:

  • x将整数格式化为十六进制值。

  • s 将任何值格式化为字符串。

  • tB 格式化一个整数为本地特定的月份名称。

还有许多其他转换。


注意:

除了 %%%n 之外,所有格式说明符都必须匹配一个参数。如果不匹配,就会抛出异常。

在 Java 编程语言中,\n 转义始终生成换行符(\u000A)。除非特别需要换行符,否则不要使用 \n。要获取本地平台的正确换行符,请使用 %n


除了转换之外,格式说明符还可以包含几个额外元素,进一步定制格式化输出。这里是一个示例,Format,使用了每种可能的元素类型。

public class Format {
    public static void main(String[] args) {
        System.out.format("%f, %1$+020.10f %n", Math.PI);
    }
}

这是输出结果:

3.141593, +00000003.1415926536

所有附加元素都是可选的。下图显示了更长格式说明符如何分解为元素。

格式说明符的元素

格式说明符的元素。

元素必须按照所示顺序出现。从右边开始,可选元素包括:

  • 精度。对于浮点值,这是格式化值的数学精度。对于 s 和其他一般转换,这是格式化值的最大宽度;如果需要,值将被右截断。

  • 宽度。格式化值的最小宽度;如果需要,将填充值。默认情况下,值左侧用空格填充。

  • 标志 指定额外的格式选项。在 Format 示例中,+ 标志指定数字应始终带有符号格式,0 标志指定 0 为填充字符。其他标志包括 -(右侧填充)和 ,(使用本地特定的千位分隔符格式化数字)。请注意,某些标志不能与其他标志或某些转换一起使用。

  • 参数索引 允许您显式匹配指定的参数。您还可以指定 < 来匹配与上一个格式说明符相同的参数。因此,示例可以这样说:System.out.format("%f, %<+020.10f %n", Math.PI);

命令行 I/O

原文:docs.oracle.com/javase/tutorial/essential/io/cl.html

程序通常从命令行运行并在命令行环境中与用户交互。Java 平台通过标准流和控制台两种方式支持这种交互。

标准流

标准流是许多操作系统的特性。默认情况下,它们从键盘读取输入并将输出写入显示器。它们还支持文件和程序之间的 I/O,但该功能由命令行解释器控制,而不是程序。

Java 平台支持三个标准流:标准输入,通过 System.in 访问;标准输出,通过 System.out 访问;以及标准错误,通过 System.err 访问。这些对象会自动定义,无需打开。标准输出和标准错误都用于输出;将错误输出单独处理允许用户将常规输出重定向到文件并仍能读取错误消息。有关更多信息,请参考您的命令行解释器文档。

你可能期望标准流是字符流,但出于历史原因,它们是字节流。System.outSystem.err 被定义为 PrintStream 对象。虽然技术上是字节流,但 PrintStream 利用内部字符流对象来模拟许多字符流的特性。

相比之下,System.in 是一个没有字符流特性的字节流。要将标准输入作为字符流使用,需要将 System.in 包装在 InputStreamReader 中。

InputStreamReader cin = new InputStreamReader(System.in);

控制台

比标准流更高级的替代方案是控制台。这是一种类型为 Console 的单一预定义对象,具有标准流提供的大部分功能,以及其他功能。控制台特别适用于安全密码输入。控制台对象还通过其 readerwriter 方法提供真正的字符流输入和输出流。

在程序可以使用控制台之前,必须通过调用 System.console() 尝试检索控制台对象。如果控制台对象可用,则此方法将返回它。如果 System.console 返回 NULL,则不允许控制台操作,可能是因为操作系统不支持它们或者因为程序在非交互环境中启动。

控制台对象通过其readPassword方法支持安全密码输入。该方法通过两种方式帮助安全密码输入。首先,它抑制回显,因此密码不会在用户屏幕上可见。其次,readPassword返回一个字符数组,而不是一个String,因此密码可以被覆盖,一旦不再需要,即从内存中删除。

Password示例是一个用于更改用户密码的原型程序。它演示了几种Console方法。

import java.io.Console;
import java.util.Arrays;
import java.io.IOException;

public class Password {

    public static void main (String args[]) throws IOException {

        Console c = System.console();
        if (c == null) {
            System.err.println("No console.");
            System.exit(1);
        }

        String login = c.readLine("Enter your login: ");
        char [] oldPassword = c.readPassword("Enter your old password: ");

        if (verify(login, oldPassword)) {
            boolean noMatch;
            do {
                char [] newPassword1 = c.readPassword("Enter your new password: ");
                char [] newPassword2 = c.readPassword("Enter new password again: ");
                noMatch = ! Arrays.equals(newPassword1, newPassword2);
                if (noMatch) {
                    c.format("Passwords don't match. Try again.%n");
                } else {
                    change(login, newPassword1);
                    c.format("Password for %s changed.%n", login);
                }
                Arrays.fill(newPassword1, ' ');
                Arrays.fill(newPassword2, ' ');
            } while (noMatch);
        }

        Arrays.fill(oldPassword, ' ');
    }

    // Dummy change method.
    static boolean verify(String login, char[] password) {
        // This method always returns
        // true in this example.
        // Modify this method to verify
        // password according to your rules.
        return true;
    }

    // Dummy change method.
    static void change(String login, char[] password) {
        // Modify this method to change
        // password according to your rules.
    }
}

Password类遵循以下步骤:

  1. 尝试检索控制台对象。如果对象不可用,则中止。

  2. 调用Console.readLine提示并读取用户的登录名。

  3. 调用Console.readPassword提示并读取用户的现有密码。

  4. 调用verify确认用户有权限更改密码。(在这个例子中,verify是一个始终返回true的虚拟方法。)

  5. 重复以下步骤,直到用户两次输入相同的密码:

    1. 两次调用Console.readPassword提示并读取新密码。

    2. 如果用户两次输入相同的密码,调用change进行更改。(同样,change是一个虚拟方法。)

    3. 用空格覆盖两个密码。

  6. 用空格覆盖旧密码。

数据流

原文:docs.oracle.com/javase/tutorial/essential/io/datastreams.html

数据流支持原始数据类型值(booleancharbyteshortintlongfloatdouble)以及String值的二进制 I/O。所有数据流都实现了DataInput接口或DataOutput接口。本节重点介绍了这些接口的最常用实现,DataInputStreamDataOutputStream

DataStreams示例演示了通过写出一组数据记录,然后再次读取它们来演示数据流。每个记录包含与发票上的项目相关的三个值,如下表所示:

记录中的顺序 数据类型 数据描述 输出方法 输入方法 示例值
1 double 项目价格 DataOutputStream.writeDouble DataInputStream.readDouble 19.99
2 int 单位数量 DataOutputStream.writeInt DataInputStream.readInt 12
3 String 项目描述 DataOutputStream.writeUTF DataInputStream.readUTF "Java T-Shirt"

让我们来看看DataStreams中关键的代码。首先,程序定义了一些包含数据文件名称和将写入其中的数据的常量:

static final String dataFile = "invoicedata";

static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
static final int[] units = { 12, 8, 13, 29, 50 };
static final String[] descs = {
    "Java T-shirt",
    "Java Mug",
    "Duke Juggling Dolls",
    "Java Pin",
    "Java Key Chain"
};

然后DataStreams打开一个输出流。由于DataOutputStream只能作为现有字节流对象的包装器创建,DataStreams提供了一个带缓冲的文件输出字节流。

out = new DataOutputStream(new BufferedOutputStream(
              new FileOutputStream(dataFile)));

DataStreams写出记录并关闭输出流。

for (int i = 0; i < prices.length; i ++) {
    out.writeDouble(prices[i]);
    out.writeInt(units[i]);
    out.writeUTF(descs[i]);
}

writeUTF方法以修改后的 UTF-8 形式写出String值。这是一种只需要一个字节来表示常见西方字符的可变宽度字符编码。

现在DataStreams再次读取数据。首先,它必须提供一个输入流和变量来保存输入数据。与DataOutputStream一样,DataInputStream必须作为字节流的包装器构建。

in = new DataInputStream(new
            BufferedInputStream(new FileInputStream(dataFile)));

double price;
int unit;
String desc;
double total = 0.0;

现在DataStreams可以读取流中的每个记录,并报告遇到的数据。

try {
    while (true) {
        price = in.readDouble();
        unit = in.readInt();
        desc = in.readUTF();
        System.out.format("You ordered %d" + " units of %s at $%.2f%n",
            unit, desc, price);
        total += unit * price;
    }
} catch (EOFException e) {
}

请注意,DataStreams通过捕获EOFException来检测文件结束条件,而不是测试无效的返回值。所有DataInput方法的实现都使用EOFException而不是返回值。

还要注意,DataStreams 中的每个专门的 write 都与相应的专门的 read 完全匹配。程序员需要确保输出类型和输入类型以这种方式匹配:输入流由简单的二进制数据组成,没有任何内容指示个别值的类型,或者它们在流中的位置。

DataStreams 使用了一种非常糟糕的编程技术:它使用浮点数来表示货币值。一般来说,浮点数对于精确值是不好的。对于十进制小数来说尤其糟糕,因为常见的值(比如0.1)没有二进制表示。

用于货币值的正确类型是java.math.BigDecimal。不幸的是,BigDecimal是一个对象类型,所以它不能与数据流一起使用。然而,BigDecimal 可以 与对象流一起使用,这将在下一节中介绍。

对象流

原文:docs.oracle.com/javase/tutorial/essential/io/objectstreams.html

就像数据流支持原始数据类型的 I/O 一样,对象流支持对象的 I/O。大多数标准类支持其对象的序列化,但并非所有类都支持。那些实现了标记接口Serializable的类支持序列化。

对象流类是ObjectInputStreamObjectOutputStream。这些类实现了ObjectInputObjectOutput,它们是DataInputDataOutput的子接口。这意味着在对象流中也实现了数据流中涵盖的所有原始数据 I/O 方法。因此,对象流可以包含原始值和对象值的混合。ObjectStreams示例说明了这一点。ObjectStreams创建了与DataStreams相同的应用程序,但有一些变化。首先,价格现在是BigDecimal对象,以更好地表示分数值。其次,一个Calendar对象被写入数据文件,表示发票日期。

如果readObject()没有返回预期的对象类型,尝试将其强制转换为正确类型可能会抛出ClassNotFoundException。在这个简单的例子中,这种情况不会发生,所以我们不尝试捕获异常。相反,我们通过将ClassNotFoundException添加到main方法的throws子句中来通知编译器,我们已经意识到了这个问题。

复杂对象的输出和输入

writeObjectreadObject方法使用起来很简单,但它们包含一些非常复杂的对象管理逻辑。对于像日历这样只封装原始值的类来说,这并不重要。但是许多对象包含对其他对象的引用。如果readObject要从流中重建一个对象,它必须能够重建原始对象引用的所有对象。这些额外的对象可能有它们自己的引用,依此类推。在这种情况下,writeObject遍历整个对象引用网络,并将该网络中的所有对象写入流中。因此,一次writeObject调用可能导致大量对象被写入流中。

这在下图中有所展示,其中调用writeObject来写入一个名为a的单一对象。这个对象包含对对象bc的引用,而b包含对de的引用。调用writeobject(a)不仅写入a,还写入了重建a所需的所有对象,因此这个网络中的其他四个对象也被写入了。当areadObject读回时,其他四个对象也被读回,并且所有原始对象引用都被保留。

多个被引用对象的 I/O

多个被引用对象的 I/O

你可能会想知道,如果同一流上的两个对象都包含对同一对象的引用会发生什么。当它们被读回时,它们会都指向同一个对象吗?答案是"是"。一个流只能包含一个对象的副本,尽管它可以包含任意数量的引用。因此,如果你明确地将一个对象两次写入流中,实际上只是写入了引用两次。例如,如果以下代码将对象ob两次写入流中:

Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);

每个writeObject都必须与一个readObject匹配,因此读取流的代码看起来会像这样:

Object ob1 = in.readObject();
Object ob2 = in.readObject();

这导致了两个变量,ob1ob2,它们都是指向同一个对象的引用。

然而,如果一个单一对象被写入两个不同的流,它实际上会被复制 — 一个程序读取这两个流将看到两个不同的对象。

文件 I/O(具有 NIO.2 功能)

译文:docs.oracle.com/javase/tutorial/essential/io/fileio.html


注意: 本教程反映了 JDK 7 版本中引入的文件 I/O 机制。Java SE 6 版本的文件 I/O 教程很简短,但您可以下载包含早期文件 I/O 内容的Java SE Tutorial 2008-03-14版本的教程。


java.nio.file包及其相关包java.nio.file.attribute为文件 I/O 和访问默认文件系统提供了全面支持。尽管 API 有许多类,但您只需关注其中的一些入口点。您会发现这个 API 非常直观和易于使用。

本教程首先询问什么是路径?然后介绍了该包的主要入口点 Path 类。解释了与语法操作相关的Path类中的方法。然后教程转向包中的另一个主要类Files类,其中包含处理文件操作的方法。首先介绍了许多 file operations 共有的一些概念。然后介绍了用于检查、删除、复制和移动文件的方法。

教程展示了在继续学习 file I/O 和 directory I/O 之前如何管理元数据。解释了随机访问文件并检查了与符号链接和硬链接相关的问题。

接下来,涵盖了一些非常强大但更高级的主题。首先演示了递归遍历文件树的能力,然后介绍了如何使用通配符搜索文件的信息。接下来,解释并演示了如何监视目录以进行更改。然后,给出了一些其他地方不适用的方法的关注。

最后,如果您在 Java SE 7 发布之前编写了文件 I/O 代码,有一个从旧 API 到新 API 的映射,以及关于File.toPath方法的重要信息,供希望利用新 API 而无需重写现有代码的开发人员参考。

什么是路径?(以及其他文件系统事实)

原文:docs.oracle.com/javase/tutorial/essential/io/path.html

文件系统在某种介质上存储和组织文件,通常是一个或多个硬盘,以便可以轻松检索文件。今天大多数使用的文件系统将文件存储在树(或分层)结构中。树的顶部是一个(或多个)根节点。在根节点下面,有文件和目录(在 Microsoft Windows 中称为文件夹)。每个目录可以包含文件和子目录,子目录又可以包含文件和子目录,依此类推,可能深入到几乎无限的深度。

本节涵盖以下内容:

  • 什么是路径?

  • 相对还是绝对?

  • 符号链接

什么是路径?

以下图显示了包含单个根节点的示例目录树。Microsoft Windows 支持多个根节点。每个根节点映射到一个卷,例如C:\D:\。Solaris OS 支持单个根节点,用斜杠字符/表示。

示例目录结构示例目录结构

文件通过其在文件系统中的路径来标识,从根节点开始。例如,在前面的图中,Solaris OS 中的statusReport文件由以下表示:

/home/sally/statusReport

在 Microsoft Windows 中,statusReport由以下表示:

C:\home\sally\statusReport

用于分隔目录名称的字符(也称为分隔符)特定于文件系统:Solaris OS 使用正斜杠(/),而 Microsoft Windows 使用反斜杠(\)。

相对还是绝对?

路径可以是相对的或绝对的。绝对路径始终包含根元素和完整的目录列表,以定位文件。例如,/home/sally/statusReport是一个绝对路径。定位文件所需的所有信息都包含在路径字符串中。

相对路径需要与另一个路径结合才能访问文件。例如,joe/foo是一个相对路径。没有更多信息,程序无法可靠地定位文件系统中的joe/foo目录。

符号链接

文件系统对象通常是目录或文件。每个人都熟悉这些对象。但是一些文件系统也支持符号链接的概念。符号链接也称为symlinksoft link

符号链接是一个特殊文件,用作指向另一个文件的引用。在大多数情况下,符号链接对应用程序是透明的,对符号链接的操作会自动重定向到链接的目标。(被指向的文件或目录称为链接的目标。)例外情况是当符号链接被删除或重命名时,链接本身被删除或重命名,而不是链接的目标。

在下图中,对用户来说,logFile看起来像是一个常规文件,但实际上它是指向dir/logs/HomeLogFile的符号链接。HomeLogFile是链接的目标。

示例符号链接符号链接示例。

对用户来说,符号链接通常是透明的。读取或写入符号链接与读取或写入任何其他文件或目录相同。

解析链接这个短语意味着用文件系统中的实际位置替换符号链接。在这个例子中,解析logFile会得到dir/logs/HomeLogFile

在现实场景中,大多数文件系统广泛使用符号链接。偶尔,粗心创建的符号链接可能会导致循环引用。循环引用发生在链接的目标指向原始链接的情况下。循环引用可能是间接的:目录a指向目录b,后者指向目录c,后者包含一个子目录指向目录a。当程序递归遍历目录结构时,循环引用可能会造成混乱。然而,这种情况已经考虑到,不会导致程序无限循环。

下一页将讨论 Java 编程语言中文件 I/O 支持的核心,即Path类。

Path 类

原文:docs.oracle.com/javase/tutorial/essential/io/pathClass.html

Path类是 Java SE 7 版本中引入的主要入口点之一,属于java.nio.file包。如果您的应用程序使用文件 I/O,您将希望了解此类的强大功能。


版本说明: 如果您有使用java.io.File的 JDK7 之前的代码,您仍然可以通过使用File.toPath方法来利用Path类的功能。有关更多信息,请参阅旧版文件 I/O 代码。


正如其名称所示,Path类是文件系统中路径的程序表示。Path对象包含用于构建路径的文件名和目录列表,并用于检查、定位和操作文件。

一个Path实例反映了底层平台。在 Solaris 操作系统中,Path使用 Solaris 语法(/home/joe/foo),而在 Microsoft Windows 中,Path使用 Windows 语法(C:\home\joe\foo)。Path不是系统独立的。你不能比较来自 Solaris 文件系统的Path并期望它与来自 Windows 文件系统的Path匹配,即使目录结构相同,两个实例都定位到相同的相对文件。

Path对应的文件或目录可能不存在。您可以创建一个Path实例并以各种方式操作它:您可以附加到它,提取它的部分,将其与另一个路径进行比较。在适当的时候,您可以使用Files类中的方法来检查与Path对应的文件的存在性,创建文件,打开文件,删除文件,更改其权限等。

下一页将详细讨论Path类。

Path 操作

原文:docs.oracle.com/javase/tutorial/essential/io/pathOps.html

Path类包括各种方法,可用于获取有关路径的信息,访问路径的元素,将路径转换为其他形式或提取路径的部分。还有用于匹配路径字符串的方法以及用于删除路径中冗余的方法。本课程介绍了这些Path方法,有时称为语法操作,因为它们作用于路径本身,而不访问文件系统。

本节涵盖以下内容:

  • 创建一个 Path

  • 检索有关 Path 的信息

  • 从 Path 中删除冗余

  • 转换 Path

  • 连接两个路径

  • 在两个路径之间创建路径

  • 比较两个路径

创建一个 Path

一个Path实例包含用于指定文件或目录位置的信息。在定义时,Path会提供一系列一个或多个名称。可能包括根元素或文件名,但都不是必需的。Path可能仅包含单个目录或文件名。

您可以通过使用Paths(注意复数形式)辅助类中的以下get方法之一轻松创建Path对象:

Path p1 = Paths.get("/tmp/foo");
Path p2 = Paths.get(args[0]);
Path p3 = Paths.get(URI.create("file:///Users/joe/FileTest.java"));

Paths.get方法是以下代码的简写:

Path p4 = FileSystems.getDefault().getPath("/users/sally");

以下示例假定您的主目录是/u/joe,则创建/u/joe/logs/foo.log,或者如果您在 Windows 上,则为C:\joe\logs\foo.log

Path p5 = Paths.get(System.getProperty("user.home"),"logs", "foo.log");

检索有关路径的信息

你可以将Path看作将这些名称元素存储为序列。目录结构中最高的元素位于索引 0。目录结构中最低的元素位于索引[n-1],其中nPath中名称元素的数量。可用于使用这些索引检索单个元素或Path的子序列的方法。

本课程中的示例使用以下目录结构。

示例目录结构示例目录结构

以下代码片段定义了一个Path实例,然后调用了几种方法以获取有关路径的信息:

// None of these methods requires that the file corresponding
// to the Path exists.
// Microsoft Windows syntax
Path path = Paths.get("C:\\home\\joe\\foo");

// Solaris syntax
Path path = Paths.get("/home/joe/foo");

System.out.format("toString: %s%n", path.toString());
System.out.format("getFileName: %s%n", path.getFileName());
System.out.format("getName(0): %s%n", path.getName(0));
System.out.format("getNameCount: %d%n", path.getNameCount());
System.out.format("subpath(0,2): %s%n", path.subpath(0,2));
System.out.format("getParent: %s%n", path.getParent());
System.out.format("getRoot: %s%n", path.getRoot());

这是 Windows 和 Solaris OS 的输出:

调用的方法 Solaris OS 中的返回 Microsoft Windows 中的返回 注释
toString /home/joe/foo C:\home\joe\foo 返回Path的字符串表示。如果路径是使用Filesystems.getDefault().getPath(String)Paths.get(后者是getPath的便利方法)创建的,则该方法会执行轻微的语法清理。例如,在 UNIX 操作系统中,它将修正输入字符串//home/joe/foo/home/joe/foo
getFileName foo foo 返回文件名或名称元素序列的最后一个元素。
getName(0) home home 返回与指定索引对应的路径元素。第 0 个元素是最靠近根的路径元素。
getNameCount 3 3 返回路径中的元素数量。
subpath(0,2) home/joe home\joe 返回Path的子序列(不包括根元素),由开始和结束索引指定。
getParent /home/joe \home\joe 返回父目录的路径。
getRoot / C:\ 返回路径的根。

前面的示例显示了绝对路径的输出。在以下示例中,指定了相对路径:

// Solaris syntax
Path path = Paths.get("sally/bar");
or
// Microsoft Windows syntax
Path path = Paths.get("sally\\bar");

以下是 Windows 和 Solaris OS 的输出:

调用的方法 Solaris OS 中返回 Microsoft Windows 中返回
toString sally/bar sally\bar
getFileName bar bar
getName(0) sally sally
getNameCount 2 2
subpath(0,1) sally sally
getParent sally sally
getRoot null null

从路径中删除冗余

许多文件系统使用“.”符号表示当前目录,“..”表示父目录。您可能会遇到路径包含冗余目录信息的情况。也许服务器配置为将其日志文件保存在“/dir/logs/.”目录中,您希望从路径中删除末尾的“/.`”符号。

以下示例都包含冗余:

/home/./joe/foo
/home/sally/../joe/foo

normalize方法会删除任何冗余元素,包括任何“.”或“directory/..”出现。前面两个示例都会规范化为/home/joe/foo`。

值得注意的是,normalize在清理路径时不会检查文件系统。这是一个纯语法操作。在第二个示例中,如果sally是一个符号链接,删除sally/..可能导致Path不再定位到预期的文件。

为了清理路径并确保结果定位到正确的文件,您可以使用toRealPath方法。该方法在下一节转换路径中描述。

转换路径

您可以使用三种方法来转换Path。如果需要将路径转换为可以从浏览器打开的字符串,可以使用toUri。例如:

Path p1 = Paths.get("/home/logfile");
// Result is file:///home/logfile
System.out.format("%s%n", p1.toUri());

toAbsolutePath方法将路径转换为绝对路径。如果传入的路径已经是绝对路径,则返回相同的Path对象。在处理用户输入的文件名时,toAbsolutePath方法非常有帮助。例如:

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

        if (args.length < 1) {
            System.out.println("usage: FileTest file");
            System.exit(-1);
        }

        // Converts the input string to a Path object.
        Path inputPath = Paths.get(args[0]);

        // Converts the input Path
        // to an absolute path.
        // Generally, this means prepending
        // the current working
        // directory.  If this example
        // were called like this:
        //     java FileTest foo
        // the getRoot and getParent methods
        // would return null
        // on the original "inputPath"
        // instance.  Invoking getRoot and
        // getParent on the "fullPath"
        // instance returns expected values.
        Path fullPath = inputPath.toAbsolutePath();
    }
}

toAbsolutePath方法转换用户输入并返回一个Path,在查询时返回有用的值。此方法不需要文件存在即可工作。

toRealPath方法返回现有文件的真实路径。该方法一次执行多个操作:

  • 如果向该方法传递true,并且文件系统支持符号链接,则该方法会解析路径中的任何符号链接。

  • 如果Path是相对路径,则返回绝对路径。

  • 如果Path包含任何多余的元素,则返回一个删除了这些元素的路径。

如果文件不存在或无法访问,则此方法会抛出异常。您可以在需要处理这些情况时捕获异常。例如:

try {
    Path fp = path.toRealPath();
} catch (NoSuchFileException x) {
    System.err.format("%s: no such" + " file or directory%n", path);
    // Logic for case when file doesn't exist.
} catch (IOException x) {
    System.err.format("%s%n", x);
    // Logic for other sort of file error.
}

连接两个路径

您可以使用resolve方法组合路径。您传入一个部分路径,即不包括根元素的路径,并将该部分路径附加到原始路径。

例如,考虑以下代码片段:

// Solaris
Path p1 = Paths.get("/home/joe/foo");
// Result is /home/joe/foo/bar
System.out.format("%s%n", p1.resolve("bar"));

or

// Microsoft Windows
Path p1 = Paths.get("C:\\home\\joe\\foo");
// Result is C:\home\joe\foo\bar
System.out.format("%s%n", p1.resolve("bar"));

将绝对路径传递给resolve方法会返回传入的路径:

// Result is /home/joe
Paths.get("foo").resolve("/home/joe");

创建两个路径之间的路径

在编写文件 I/O 代码时,通常需要能够构造从文件系统中的一个位置到另一个位置的路径。您可以使用relativize方法来实现这一点。该方法构造一个源自原始路径并以传入路径指定的位置结束的路径。新路径是相对于原始路径的。

例如,考虑两个定义为joesally的相对路径:

Path p1 = Paths.get("joe");
Path p2 = Paths.get("sally");

在没有其他信息的情况下,假定joesally是兄弟姐妹,即在树结构中处于同一级别的节点。要从joe导航到sally,你需要先向上导航一级到父节点,然后再向下导航到sally

// Result is ../sally
Path p1_to_p2 = p1.relativize(p2);
// Result is ../joe
Path p2_to_p1 = p2.relativize(p1);

考虑一个稍微复杂的例子:

Path p1 = Paths.get("home");
Path p3 = Paths.get("home/sally/bar");
// Result is sally/bar
Path p1_to_p3 = p1.relativize(p3);
// Result is ../..
Path p3_to_p1 = p3.relativize(p1);

在这个例子中,这两个路径共享相同的节点home。要从home导航到bar,你首先向下导航一级到sally,然后再向下导航一级到bar。从barhome的导航需要向上移动两级。

如果只有一个路径包含根元素,则无法构造相对路径。如果两个路径都包含根元素,则构造相对路径的能力取决于系统。

递归Copy示例使用relativizeresolve方法。

比较两个路径

Path类支持equals,使您能够测试两个路径是否相等。startsWithendsWith方法使您能够测试路径是否以特定字符串开头或结尾。这些方法易于使用。例如:

Path path = ...;
Path otherPath = ...;
Path beginning = Paths.get("/home");
Path ending = Paths.get("foo");

if (path.equals(otherPath)) {
    // *equality logic here*
} else if (path.startsWith(beginning)) {
    // *path begins with "/home"*
} else if (path.endsWith(ending)) {
    // *path ends with "foo"*
}

Path类实现了Iterable接口。iterator方法返回一个对象,使你能够遍历路径中的名称元素。返回的第一个元素是在目录树中最接近根的元素。以下代码片段遍历一个路径,打印每个名称元素:

Path path = ...;
for (Path name: path) {
    System.out.println(name);
}

Path类还实现了Comparable接口。你可以使用compareTo比较Path对象,这对于排序很有用。

你也可以将Path对象放入Collection中。查看 Collections trail,了解更多关于这一强大功能的信息。

当你想要验证两个Path对象是否定位到同一文件时,可以使用isSameFile方法,如 Checking Whether Two Paths Locate the Same File 中所述。

文件操作

原文:docs.oracle.com/javase/tutorial/essential/io/fileOps.html

Files类是java.nio.file包的另一个主要入口点。该类提供了丰富的静态方法集,用于读取、写入和操作文件和目录。Files方法适用于Path对象的实例。在继续阅读其余部分之前,您应该熟悉以下常见概念:

  • 释放系统资源

  • 捕获异常

  • 可变参数

  • 原子操作

  • 方法链

  • 什么是 Glob?

  • 链接感知

释放系统资源

此 API 中使用的许多资源,如流或通道,实现或扩展了java.io.Closeable接口。Closeable资源的要求是在不再需要时必须调用close方法来释放资源。忽略关闭资源可能会对应用程序的性能产生负面影响。下一节描述的try-with-resources 语句会为您处理此步骤。

捕获异常

在文件 I/O 中,意外情况是生活中的一个事实:文件存在(或不存在)时预期的,程序无法访问文件系统,默认文件系统实现不支持特定功能,等等。可能会遇到许多错误。

所有访问文件系统的方法都可能抛出IOException。最佳做法是通过将这些方法嵌入到 Java SE 7 版本中引入的try-with-resources 语句中来捕获这些异常。try-with-resources 语句的优点是编译器在不再需要时会自动生成关闭资源的代码。以下代码显示了这种情况可能如何:

Charset charset = Charset.forName("US-ASCII");
String s = ...;
try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
    writer.write(s, 0, s.length());
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}

有关更多信息,请参阅 try-with-resources 语句。

或者,您可以将文件 I/O 方法嵌入到try块中,然后在catch块中捕获任何异常。如果您的代码已打开任何流或通道,则应在finally块中关闭它们。使用 try-catch-finally 方法的前一个示例可能如下所示:

Charset charset = Charset.forName("US-ASCII");
String s = ...;
BufferedWriter writer = null;
try {
    writer = Files.newBufferedWriter(file, charset);
    writer.write(s, 0, s.length());
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
} finally {
    if (writer != null) writer.close();
}

有关更多信息,请参阅捕获和处理异常。

除了IOException,许多特定异常扩展了FileSystemException。这个类有一些有用的方法,返回涉及的文件(getFile),详细的消息字符串(getMessage),文件系统操作失败的原因(getReason),以及涉及的“其他”文件(如果有)(getOtherFile)。

以下代码片段展示了getFile方法可能如何使用:

try (...) {
    ...    
} catch (NoSuchFileException x) {
    System.err.format("%s does not exist\n", x.getFile());
}

为了清晰起见,本课程中的文件 I/O 示例可能不显示异常处理,但您的代码应始终包含它。

可变参数

当指定标志时,几个Files方法接受任意数量的参数。例如,在以下方法签名中,CopyOption参数后的省略号表示该方法接受可变数量的参数,通常称为可变参数

Path Files.move(Path, Path, CopyOption...)

当一个方法接受可变参数时,您可以传递逗号分隔的值列表或值数组(CopyOption[])。

move示例中,该方法可以如下调用:

import static java.nio.file.StandardCopyOption.*;

Path source = ...;
Path target = ...;
Files.move(source,
           target,
           REPLACE_EXISTING,
           ATOMIC_MOVE);

有关可变参数语法的更多信息,请参见可变参数。

原子操作

几个Files方法,比如move,可以在一些文件系统中以原子方式执行某些操作。

原子文件操作是一种不能被中断或“部分”执行的操作。要么整个操作被执行,要么操作失败。当您有多个进程在文件系统的同一区域上操作,并且您需要保证每个进程访问完整的文件时,这一点就很重要。

方法链

许多文件 I/O 方法支持方法链的概念。

首先调用返回对象的方法。然后立即在对象上调用一个方法,该方法返回另一个对象,依此类推。许多 I/O 示例使用以下技术:

String value = Charset.defaultCharset().decode(buf).toString();
UserPrincipal group =
    file.getFileSystem().getUserPrincipalLookupService().
         lookupPrincipalByName("me");

这种技术产生了紧凑的代码,并使您能够避免声明您不需要的临时变量。

什么是通配符

Files类中有两个方法接受通配符参数,但是通配符是什么?

您可以使用通配符语法来指定模式匹配行为。

通配符模式被指定为一个字符串,并与其他字符串(如目录或文件名)匹配。通配符语法遵循几个简单的规则:

  • 星号*匹配任意数量的字符(包括零个)。

  • 两个星号**的工作方式类似于*,但跨越目录边界。这种语法通常用于匹配完整路径。

  • 问号,?,匹配恰好一个字符。

  • 大括号指定一组子模式。例如:

    • {太阳,月亮,星星} 匹配"太阳"、"月亮"或"星星"。

    • {temp*,tmp*} 匹配所有以"temp"或"tmp"开头的字符串。

  • 方括号传达一组单个字符,或者当使用连字符字符(-)时,传达一组字符范围。例如:

    • [aeiou] 匹配任何小写元音字母。

    • [0-9] 匹配任何数字。

    • [A-Z] 匹配任何大写字母。

    • [a-z,A-Z] 匹配任何大写或小写字母。在方括号内,*?\代表它们自身。

  • 所有其他字符都代表它们自身。

  • 要匹配*?或其他特殊字符,可以使用反斜杠字符\进行转义。例如:\\匹配一个反斜杠,\?匹配问号。

以下是一些通配符语法示例:

  • *.html – 匹配所有以 .html 结尾的字符串

  • ??? – 匹配所有恰好有三个字母或数字的字符串

  • *[0-9]* – 匹配所有包含数字值的字符串

  • *.{htm,html,pdf} – 匹配任何以 .htm.html.pdf 结尾的字符串

  • a?*.java – 匹配任何以 a 开头,后跟至少一个字母或数字,并以 .java 结尾的字符串

  • {foo*,*[0-9]*} – 匹配任何以 foo 开头的字符串或任何包含数字值的字符串


注意: 如果您正在键盘上输入通配符模式,并且其中包含一个特殊字符,您必须将模式放在引号中("*"),使用反斜杠(\*),或使用命令行支持的任何转义机制。


通配符语法功能强大且易于使用。但是,如果它不满足您的需求,您也可以使用正则表达式。有关更多信息,请参阅正则表达式 课程。

有关通配符语法的更多信息,请参阅FileSystem 类中 getPathMatcher 方法的 API 规范。

链接感知

Files 类是"链接感知"的。每个 Files 方法都会检测在遇到符号链接时该做什么,或者提供一个选项,使您能够配置在遇到符号链接时的行为。

检查文件或目录

原文:docs.oracle.com/javase/tutorial/essential/io/check.html

您有一个代表文件或目录的Path实例,但该文件是否存在于文件系统中?它是否可读?可写?可执行?

验证文件或目录的存在

Path类中的方法是语法的,意味着它们在Path实例上操作。但最终您必须访问文件系统来验证特定的Path是否存在或不存在。您可以使用exists(Path, LinkOption...)notExists(Path, LinkOption...)方法。请注意,!Files.exists(path)并不等同于Files.notExists(path)。当您测试文件的存在时,可能有三种结果:

  • 验证文件存在。

  • 验证文件不存在。

  • 文件的状态未知。当程序无法访问文件时,会出现此结果。

如果existsnotExists都返回false,则无法验证文件的存在。

检查文件的可访问性

要验证程序是否可以按需访问文件,可以使用isReadable(Path)isWritable(Path)isExecutable(Path)方法。

以下代码片段验证特定文件是否存在,并且程序是否有执行文件的能力。

Path file = ...;
boolean isRegularExecutableFile = Files.isRegularFile(file) &
         Files.isReadable(file) & Files.isExecutable(file);


注意: 一旦其中任何方法完成,就不能保证可以访问文件。许多应用程序中的常见安全漏洞是执行检查然后访问文件。要获取更多信息,请使用您喜欢的搜索引擎查找TOCTTOU(发音为TOCK-too)。


检查两个路径是否定位到相同文件

当您有一个使用符号链接的文件系统时,可能会有两个不同的路径定位到同一个文件。isSameFile(Path, Path)方法比较两个路径,以确定它们是否在文件系统上定位到同一个文件。例如:

Path p1 = ...;
Path p2 = ...;

if (Files.isSameFile(p1, p2)) {
    // Logic when the paths locate the same file
}

删除文件或目录

原文:docs.oracle.com/javase/tutorial/essential/io/delete.html

您可以删除文件、目录或链接。对于符号链接,删除的是链接本身而不是链接的目标。对于目录,目录必须为空,否则删除操作将失败。

Files类提供了两种删除方法。

delete(Path)方法会删除文件,如果删除失败则会抛出异常。例如,如果文件不存在,则会抛出NoSuchFileException异常。您可以捕获异常以确定删除失败的原因,如下所示:

try {
    Files.delete(path);
} catch (NoSuchFileException x) {
    System.err.format("%s: no such" + " file or directory%n", path);
} catch (DirectoryNotEmptyException x) {
    System.err.format("%s not empty%n", path);
} catch (IOException x) {
    // File permission problems are caught here.
    System.err.println(x);
}

deleteIfExists(Path)方法也会删除文件,但如果文件不存在,则不会抛出异常。静默失败在您有多个线程删除文件时很有用,您不希望仅因为一个线程先执行删除操作就抛出异常。

复制文件或目录

原文:docs.oracle.com/javase/tutorial/essential/io/copy.html

您可以使用copy(Path, Path, CopyOption...)方法复制文件或目录。如果目标文件存在,则复制将失败,除非指定了REPLACE_EXISTING选项。

目录可以被复制。但是,目录内的文件不会被复制,因此即使原始目录包含文件,新目录也是空的。

复制符号链接时,会复制链接的目标。如果要复制链接本身而不是链接的内容,请指定NOFOLLOW_LINKSREPLACE_EXISTING选项。

这种方法接受可变参数。支持以下StandardCopyOptionLinkOption枚举:

  • REPLACE_EXISTING – 即使目标文件已经存在,也执行复制。如果目标是符号链接,则复制链接本身(而不是链接的目标)。如果目标是非空目录,则复制将失败,并显示DirectoryNotEmptyException异常。

  • COPY_ATTRIBUTES – 复制与文件关联的文件属性到目标文件。支持的确切文件属性取决于文件系统和平台,但last-modified-time在各个平台上都受支持,并且会被复制到目标文件。

  • NOFOLLOW_LINKS – 表示不应跟随符号链接。如果要复制的文件是符号链接,则复制链接本身(而不是链接的目标)。

如果您不熟悉enums,请参见枚举类型。

以下显示了如何使用copy方法:

import static java.nio.file.StandardCopyOption.*;
...
Files.copy(source, target, REPLACE_EXISTING);

除了文件复制外,Files类还定义了可用于文件和流之间复制的方法。copy(InputStream, Path, CopyOptions...)方法可用于将输入流的所有字节复制到文件。copy(Path, OutputStream)方法可用于将文件的所有字节复制到输出流。

Copy示例使用copyFiles.walkFileTree方法支持递归复制。有关更多信息,请参见遍历文件树。

移动文件或目录

原文:docs.oracle.com/javase/tutorial/essential/io/move.html

您可以使用move(Path, Path, CopyOption...)方法移动文件或目录。如果目标文件存在,则移动失败,除非指定了REPLACE_EXISTING选项。

空目录可以被移动。如果目录不为空,只有当可以移动目录而不移动该目录的内容时才允许移动。在 UNIX 系统上,将目录移动到同一分区通常是重命名目录。在这种情况下,即使目录包含文件,此方法也有效。

此方法接受可变参数 – 支持以下StandardCopyOption枚举:

  • REPLACE_EXISTING – 即使目标文件已经存在,也执行移动操作。如果目标是一个符号链接,符号链接会被替换,但它指向的内容不受影响。

  • ATOMIC_MOVE – 将移动操作作为原子文件操作执行。如果文件系统不支持原子移动,则会抛出异常。使用ATOMIC_MOVE,您可以将文件移动到目录中,并确保任何监视目录的进程访问完整文件。

以下展示了如何使用move方法:

import static java.nio.file.StandardCopyOption.*;
...
Files.move(source, target, REPLACE_EXISTING);

尽管可以像所示在单个目录上实现move方法,但该方法通常与文件树递归机制一起使用。有关更多信息,请参见遍历文件树。

管理元数据(文件和文件存储属性)

原文:docs.oracle.com/javase/tutorial/essential/io/fileAttr.html

元数据的定义是“关于其他数据的数据”。在文件系统中,数据包含在其文件和目录中,元数据跟踪每个对象的信息:它是一个常规文件、目录还是链接?它的大小、创建日期、最后修改日期、文件所有者、组所有者和访问权限是什么?

文件系统的元数据通常被称为其文件属性Files类包括可用于获取文件的单个属性或设置属性的方法。

方法 说明
size(Path) 返回指定文件的大小(以字节为单位)。
isDirectory(Path, LinkOption) 如果指定的Path定位到一个目录,则返回 true。
isRegularFile(Path, LinkOption...) 如果指定的Path定位到一个常规文件,则返回 true。
isSymbolicLink(Path) 如果指定的Path定位到一个符号链接文件,则返回 true。
isHidden(Path) 如果指定的Path定位到文件系统认为是隐藏的文件,则返回 true。
getLastModifiedTime(Path, LinkOption...) setLastModifiedTime(Path, FileTime) 返回或设置指定文件的最后修改时间。
getOwner(Path, LinkOption...) setOwner(Path, UserPrincipal) 返回或设置文件的所有者。
getPosixFilePermissions(Path, LinkOption...) setPosixFilePermissions(Path, Set<PosixFilePermission>) 返回或设置文件的 POSIX 文件权限。
getAttribute(Path, String, LinkOption...) setAttribute(Path, String, Object, LinkOption...) 返回或设置文件属性的值。

如果程序需要同时获取多个文件属性,使用检索单个属性的方法可能效率低下。反复访问文件系统以检索单个属性可能会对性能产生不利影响。因此,Files 类提供了两个 readAttributes 方法,以便一次性获取文件的属性。

方法 注释
readAttributes(Path, String, LinkOption...) 作为批量操作读取文件的属性。String 参数标识要读取的属性。
readAttributes(Path, Class<A>, LinkOption...) 作为批量操作读取文件的属性。Class<A> 参数是请求的属性类型,该方法返回该类的对象。

在展示 readAttributes 方法的示例之前,应该提到不同的文件系统对应该跟踪哪些属性有不同的概念。因此,相关文件属性被分组到视图中。视图 映射到特定的文件系统实现,如 POSIX 或 DOS,或者映射到常见功能,如文件所有权。

支持的视图如下:

  • BasicFileAttributeView – 提供必须由所有文件系统实现支持的基本属性视图。

  • DosFileAttributeView – 将基本属性视图扩展为在支持 DOS 属性的文件系统上支持的标准四位。

  • PosixFileAttributeView – 将基本属性视图扩展到支持 POSIX 标准族的文件系统上支持的属性,例如 UNIX。这些属性包括文件所有者、组所有者和九个相关的访问权限。

  • FileOwnerAttributeView – 受到支持文件所有者概念的任何文件系统实现支持。

  • AclFileAttributeView – 支持读取或更新文件的访问控制列表(ACL)。支持 NFSv4 ACL 模型。任何 ACL 模型,例如 Windows ACL 模型,如果与 NFSv4 模型有明确定义的映射,也可能受支持。

  • UserDefinedFileAttributeView – 支持用户定义的元数据。此视图可以映射到系统支持的任何扩展机制。例如,在 Solaris 操作系统中,您可以使用此视图存储文件的 MIME 类型。

特定的文件系统实现可能仅支持基本文件属性视图,或者可能支持其中几个文件属性视图。文件系统实现可能支持此 API 中未包含的其他属性视图。

在大多数情况下,您不应直接处理任何 FileAttributeView 接口。(如果您确实需要直接使用 FileAttributeView,可以通过 getFileAttributeView(Path, Class<V>, LinkOption...) 方法访问它。)

readAttributes 方法使用泛型,可用于读取任何文件属性视图的属性。本页其余部分的示例使用 readAttributes 方法。

本节的其余部分涵盖以下主题:

  • 基本文件属性

  • 设置时间戳

  • DOS 文件属性

  • POSIX 文件权限

  • 设置文件或组所有者

  • 用户定义的文件属性

  • 文件存储属性

基本文件属性

如前所述,要读取文件的基本属性,可以使用 Files.readAttributes 方法之一,该方法一次性读取所有基本属性。这比单独访问文件系统以读取每个单独属性要高效得多。可变参数参数当前支持 LinkOption 枚举,NOFOLLOW_LINKS。当您不希望跟随符号链接时,请使用此选项。


关于时间戳的说明:基本属性集包括三个时间戳:creationTimelastModifiedTimelastAccessTime。 在特定实现中,这些时间戳中的任何一个可能不受支持,如果不支持,则相应的访问器方法返回一个特定于实现的值。 当支持时,时间戳作为FileTime对象返回。


以下代码片段读取并打印给定文件的基本文件属性,并使用BasicFileAttributes类中的方法。

Path file = ...;
BasicFileAttributes attr = Files.readAttributes(file, BasicFileAttributes.class);

System.out.println("creationTime: " + attr.creationTime());
System.out.println("lastAccessTime: " + attr.lastAccessTime());
System.out.println("lastModifiedTime: " + attr.lastModifiedTime());

System.out.println("isDirectory: " + attr.isDirectory());
System.out.println("isOther: " + attr.isOther());
System.out.println("isRegularFile: " + attr.isRegularFile());
System.out.println("isSymbolicLink: " + attr.isSymbolicLink());
System.out.println("size: " + attr.size());

除了本示例中显示的访问器方法之外,还有一个fileKey方法,返回一个唯一标识文件的对象,如果没有文件键可用,则返回null

设置时间戳

以下代码片段设置最后修改时间(以毫秒为单位):

Path file = ...;
BasicFileAttributes attr =
    Files.readAttributes(file, BasicFileAttributes.class);
long currentTime = System.currentTimeMillis();
FileTime ft = FileTime.fromMillis(currentTime);
Files.setLastModifiedTime(file, ft);
}

DOS 文件属性

DOS 文件属性也受支持于除 DOS 外的其他文件系统,如 Samba。 以下代码片段使用DosFileAttributes类的方法。

Path file = ...;
try {
    DosFileAttributes attr =
        Files.readAttributes(file, DosFileAttributes.class);
    System.out.println("isReadOnly is " + attr.isReadOnly());
    System.out.println("isHidden is " + attr.isHidden());
    System.out.println("isArchive is " + attr.isArchive());
    System.out.println("isSystem is " + attr.isSystem());
} catch (UnsupportedOperationException x) {
    System.err.println("DOS file" +
        " attributes not supported:" + x);
}

但是,您可以使用setAttribute(Path, String, Object, LinkOption...)方法设置 DOS 属性,如下所示:

Path file = ...;
Files.setAttribute(file, "dos:hidden", true);

POSIX 文件权限

POSIX是 UNIX 可移植操作系统接口的缩写,是一组 IEEE 和 ISO 标准,旨在确保不同 UNIX 变种之间的互操作性。 如果程序符合这些 POSIX 标准,它应该很容易移植到其他符合 POSIX 标准的操作系统。

除了文件所有者和组所有者,POSIX 还支持九种文件权限:文件所有者、同一组成员和“其他所有人”的读取、写入和执行权限。

以下代码片段读取给定文件的 POSIX 文件属性,并将其打印到标准输出。 该代码使用PosixFileAttributes类中的方法。

Path file = ...;
PosixFileAttributes attr =
    Files.readAttributes(file, PosixFileAttributes.class);
System.out.format("%s %s %s%n",
    attr.owner().getName(),
    attr.group().getName(),
    PosixFilePermissions.toString(attr.permissions()));

PosixFilePermissions辅助类提供了几个有用的方法,如下所示:

  • toString方法,在前面的代码片段中使用,将文件权限转换为字符串(例如,rw-r--r--)。

  • fromString方法接受表示文件权限的字符串并构造一个文件权限的Set

  • asFileAttribute方法接受一个文件权限的Set并构造一个可传递给Path.createFilePath.createDirectory方法的文件属性。

以下代码片段从一个文件中读取属性并创建一个新文件,将原始文件的属性分配给新文件:

Path sourceFile = ...;
Path newFile = ...;
PosixFileAttributes attrs =
    Files.readAttributes(sourceFile, PosixFileAttributes.class);
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(attrs.permissions());
Files.createFile(file, attr);

asFileAttribute方法将权限包装为FileAttribute。然后代码尝试使用这些权限创建一个新文件。请注意,umask也适用,因此新文件可能比请求的权限更安全。

要将文件的权限设置为表示为硬编码字符串的值,可以使用以下代码:

Path file = ...;
Set<PosixFilePermission> perms =
    PosixFilePermissions.fromString("rw-------");
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(perms);
Files.setPosixFilePermissions(file, perms);

Chmod示例递归更改文件权限,类似于chmod实用程序。

设置文件或组所有者

要将名称翻译为可存储为文件所有者或组所有者的对象,您可以使用UserPrincipalLookupService服务。此服务查找一个字符串作为名称或组名称,并返回表示该字符串的UserPrincipal对象。您可以通过使用FileSystem.getUserPrincipalLookupService方法获取默认文件系统的用户主体查找服务。

以下代码片段显示了如何使用setOwner方法设置文件所有者:

Path file = ...;
UserPrincipal owner = file.GetFileSystem().getUserPrincipalLookupService()
        .lookupPrincipalByName("sally");
Files.setOwner(file, owner);

Files类中没有专门用于设置组所有者的方法。但是,直接通过 POSIX 文件属性视图安全地执行此操作的方法如下:

Path file = ...;
GroupPrincipal group =
    file.getFileSystem().getUserPrincipalLookupService()
        .lookupPrincipalByGroupName("green");
Files.getFileAttributeView(file, PosixFileAttributeView.class)
     .setGroup(group);

用户定义的文件属性

如果您的文件系统实现支持的文件属性不足以满足您的需求,您可以使用UserDefinedAttributeView来创建和跟踪自己的文件属性。

一些实现将这个概念映射到诸如 NTFS 替代数据流和文件系统(如 ext3 和 ZFS)上的扩展属性等功能。大多数实现对值的大小施加限制,例如,ext3 将大小限制为 4 千字节。

文件的 MIME 类型可以使用此代码片段存储为用户定义的属性:

Path file = ...;
UserDefinedFileAttributeView view = Files
    .getFileAttributeView(file, UserDefinedFileAttributeView.class);
view.write("user.mimetype",
           Charset.defaultCharset().encode("text/html");

要读取 MIME 类型属性,您可以使用以下代码片段:

Path file = ...;
UserDefinedFileAttributeView view = Files
.getFileAttributeView(file,UserDefinedFileAttributeView.class);
String name = "user.mimetype";
ByteBuffer buf = ByteBuffer.allocate(view.size(name));
view.read(name, buf);
buf.flip();
String value = Charset.defaultCharset().decode(buf).toString();

Xdd示例显示了如何获取、设置和删除用户定义的属性。


注意:在 Linux 中,您可能需要启用扩展属性才能使用户定义的属性起作用。如果尝试访问用户定义的属性视图时收到UnsupportedOperationException,则需要重新挂载文件系统。以下命令重新挂载具有 ext3 文件系统扩展属性的根分区。如果此命令对您的 Linux 版本不起作用,请参考文档。

$ sudo mount -o remount,user_xattr /

如果要使更改永久生效,请向/etc/fstab添加条目。


文件存储属性

您可以使用FileStore类来了解文件存储的信息,例如可用空间有多少。getFileStore(Path)方法获取指定文件的文件存储。

以下代码片段打印了特定文件所在文件存储的空间使用情况:

Path file = ...;
FileStore store = Files.getFileStore(file);

long total = store.getTotalSpace() / 1024;
long used = (store.getTotalSpace() -
             store.getUnallocatedSpace()) / 1024;
long avail = store.getUsableSpace() / 1024;

DiskUsage示例使用此 API 打印默认文件系统中所有存储的磁盘空间信息。此示例使用FileSystem类中的getFileStores方法来获取文件系统的所有文件存储。

读取、写入和创建文件

原文:docs.oracle.com/javase/tutorial/essential/io/file.html

本页讨论了读取、写入、创建和打开文件的详细信息。有各种文件 I/O 方法可供选择。为了帮助理解 API,以下图表按复杂性排列了文件 I/O 方法。

从最不复杂(左侧)到最复杂(右侧)排列的文件 I/O 方法的线条图。从简单到复杂排列的文件 I/O 方法

在图表的最左侧是实用方法readAllBytesreadAllLineswrite方法,设计用于简单、常见情况。在这些方法的右侧是用于迭代流或文本行的方法,如newBufferedReadernewBufferedWriter,然后是newInputStreamnewOutputStream。这些方法与java.io包兼容。在这些方法的右侧是处理ByteChannelsSeekableByteChannelsByteBuffers的方法,如newByteChannel方法。最后,在最右侧是使用FileChannel进行文件锁定或内存映射 I/O 的高级应用的方法。


注意: 创建新文件的方法使您能够为文件指定一组可选的初始属性。例如,在支持 POSIX 标准集(如 UNIX)的文件系统上,您可以在创建文件时指定文件所有者、组所有者或文件权限。管理元数据页面解释了文件属性,以及如何访问和设置它们。


本页包含以下主题:

  • 打开选项

  • 小文件常用方法

  • 文本文件的缓冲 I/O 方法

  • 无缓冲流和与java.io API 兼容的方法

  • 通道和ByteBuffers的方法

  • 创建常规和临时文件的方法


打开选项

本节中的几种方法采用可选的OpenOptions参数。此参数是可选的,API 会告诉您当未指定时方法的默认行为是什么。

支持以下StandardOpenOptions枚举:

  • WRITE – 打开文件以进行写访问。

  • APPEND – 将新数据追加到文件末尾。此选项与WRITECREATE选项一起使用。

  • TRUNCATE_EXISTING – 将文件截断为零字节。此选项与WRITE选项一起使用。

  • CREATE_NEW – 创建新文件,并在文件已存在时抛出异常。

  • CREATE – 如果文件存在则打开文件,如果不存在则创建新文件。

  • DELETE_ON_CLOSE – 在流关闭时删除文件。此选项对临时文件很有用。

  • SPARSE – 暗示新创建的文件将是稀疏的。这个高级选项在一些文件系统上得到支持,比如 NTFS,在这些文件系统中,具有数据“间隙”的大文件可以以更有效的方式存储,其中这些空白间隙不会占用磁盘空间。

  • SYNC – 保持文件(内容和元数据)与底层存储设备同步。

  • DSYNC – 保持文件内容与底层存储设备同步。


小文件常用方法

从文件中读取所有字节或行

如果您有一个相对较小的文件,并且希望一次读取其全部内容,您可以使用readAllBytes(Path)readAllLines(Path, Charset)方法。这些方法会为您处理大部分工作,比如打开和关闭流,但不适用于处理大文件。以下代码展示了如何使用readAllBytes方法:

Path file = ...;
byte[] fileArray;
fileArray = Files.readAllBytes(file);

将所有字节或行写入文件

您可以使用其中一种写入方法将字节或行写入文件。

  • write(Path, byte[], OpenOption...)

  • write(Path, Iterable< extends CharSequence>, Charset, OpenOption...)

以下代码片段展示了如何使用write方法。

Path file = ...;
byte[] buf = ...;
Files.write(file, buf);


用于文本文件的缓冲 I/O 方法

java.nio.file包支持通道 I/O,它通过缓冲区传输数据,绕过了一些可能成为流 I/O 瓶颈的层。

使用缓冲流 I/O 读取文件

newBufferedReader(Path, Charset)方法打开一个文件进行读取,返回一个BufferedReader,可用于以高效的方式从文件中读取文本。

以下代码片段展示了如何使用newBufferedReader方法从文件中读取内容。该文件以"US-ASCII"编码。

Charset charset = Charset.forName("US-ASCII");
try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
    String line = null;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}

使用缓冲流 I/O 写入文件

您可以使用newBufferedWriter(Path, Charset, OpenOption...)方法使用BufferedWriter写入文件。

以下代码片段展示了如何使用这种方法创建一个以"US-ASCII"编码的文件:

Charset charset = Charset.forName("US-ASCII");
String s = ...;
try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
    writer.write(s, 0, s.length());
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}


用于非缓冲流和与java.ioAPI 互操作的方法

使用流 I/O 读取文件

要打开文件进行读取,您可以使用newInputStream(Path, OpenOption...)方法。该方法返回一个用于从文件中读取字节的无缓冲输入流。

Path file = ...;
try (InputStream in = Files.newInputStream(file);
    BufferedReader reader =
      new BufferedReader(new InputStreamReader(in))) {
    String line = null;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException x) {
    System.err.println(x);
}

通过流 I/O 创建和写入文件

您可以通过使用newOutputStream(Path, OpenOption...)方法创建文件,追加到文件或向文件写入内容。该方法打开或创建一个文件以写入字节,并返回一个无缓冲的输出流。

该方法接受一个可选的OpenOption参数。如果未指定任何打开选项,并且文件不存在,则会创建一个新文件。如果文件存在,则会被截断。此选项等同于使用CREATETRUNCATE_EXISTING选项调用该方法。

以下示例打开一个日志文件。如果文件不存在,则会创建该文件。如果文件存在,则会以追加方式打开。

import static java.nio.file.StandardOpenOption.*;
import java.nio.file.*;
import java.io.*;

public class LogFileTest {

  public static void main(String[] args) {

    // Convert the string to a
    // byte array.
    String s = "Hello World! ";
    byte data[] = s.getBytes();
    Path p = Paths.get("./logfile.txt");

    try (OutputStream out = new BufferedOutputStream(
      Files.newOutputStream(p, CREATE, APPEND))) {
      out.write(data, 0, data.length);
    } catch (IOException x) {
      System.err.println(x);
    }
  }
}


通道和ByteBuffers的方法

通过使用通道 I/O 读取和写入文件

流 I/O 每次读取一个字符,而通道 I/O 每次读取一个缓冲区。ByteChannel接口提供基本的readwrite功能。SeekableByteChannel是一个具有在通道中保持位置并更改该位置能力的ByteChannelSeekableByteChannel还支持截断与通道关联的文件并查询文件的大小。

在文件中移动到不同位置然后从该位置读取或写入使得文件的随机访问成为可能。查看随机访问文件获取更多信息。

有两种读取和写入通道 I/O 的方法。

  • newByteChannel(Path, OpenOption...)

  • newByteChannel(Path, Set<? extends OpenOption>, FileAttribute<?>...)


注意: newByteChannel方法返回一个SeekableByteChannel的实例。对于默认文件系统,您可以将此可寻址字节通道转换为FileChannel,从而提供对更高级功能的访问,例如将文件的某个区域直接映射到内存以实现更快的访问,锁定文件的某个区域以防其他进程访问,或者从绝对位置读取和写入字节而不影响通道的当前位置。


两个newByteChannel方法都允许你指定一系列OpenOption选项。除了支持newOutputStream方法使用的相同打开选项外,还支持一个额外的选项:READ是必需的,因为SeekableByteChannel支持读取和写入。

指定READ打开通道以进行读取。指定WRITEAPPEND打开通道以进行写入。如果没有指定这些选项中的任何一个,则通道将被打开以进行读取。

下面的代码片段读取一个文件并将其打印到标准输出:

public static void readFile(Path path) throws IOException {

    // Files.newByteChannel() defaults to StandardOpenOption.READ
    try (SeekableByteChannel sbc = Files.newByteChannel(path)) {
        final int BUFFER_CAPACITY = 10;
        ByteBuffer buf = ByteBuffer.allocate(BUFFER_CAPACITY);

        // Read the bytes with the proper encoding for this platform. If
        // you skip this step, you might see foreign or illegible
        // characters.
        String encoding = System.getProperty("file.encoding");
        while (sbc.read(buf) > 0) {
            buf.flip();
            System.out.print(Charset.forName(encoding).decode(buf));
            buf.clear();
        }
    }    
}

下面的示例是为 UNIX 和其他 POSIX 文件系统编写的,它创建了一个具有特定文件权限集的日志文件。这段代码将创建一个日志文件,如果该文件已经存在,则追加到日志文件中。该日志文件为所有者提供读写权限,为组提供只读权限。

import static java.nio.file.StandardOpenOption.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
import java.util.*;

public class LogFilePermissionsTest {

  public static void main(String[] args) {

    // Create the set of options for appending to the file.
    Set<OpenOption> options = new HashSet<OpenOption>();
    options.add(APPEND);
    options.add(CREATE);

    // Create the custom permissions attribute.
    Set<PosixFilePermission> perms =
      PosixFilePermissions.fromString("rw-r-----");
    FileAttribute<Set<PosixFilePermission>> attr =
      PosixFilePermissions.asFileAttribute(perms);

    // Convert the string to a ByteBuffer.
    String s = "Hello World! ";
    byte data[] = s.getBytes();
    ByteBuffer bb = ByteBuffer.wrap(data);

    Path file = Paths.get("./permissions.log");

    try (SeekableByteChannel sbc =
      Files.newByteChannel(file, options, attr)) {
      sbc.write(bb);
    } catch (IOException x) {
      System.out.println("Exception thrown: " + x);
    }
  }
}


创建常规和临时文件的方法

创建文件

你可以使用createFile(Path, FileAttribute<?>)方法创建一个具有初始属性集的空文件。例如,如果在创建时你想要文件具有特定的文件权限集,可以使用createFile方法来实现。如果你没有指定任何属性,文件将使用默认属性创建。如果文件已经存在,createFile会抛出异常。

在单个原子操作中,createFile方法检查文件是否存在,并使用指定的属性创建该文件,这使得该过程更加安全,防止恶意代码。

下面的代码片段创建一个具有默认属性的文件:

Path file = ...;
try {
    // Create the empty file with default permissions, etc.
    Files.createFile(file);
} catch (FileAlreadyExistsException x) {
    System.err.format("file named %s" +
        " already exists%n", file);
} catch (IOException x) {
    // Some other sort of failure, such as permissions.
    System.err.format("createFile error: %s%n", x);
}

POSIX 文件权限有一个示例,使用createFile(Path, FileAttribute<?>)创建一个具有预设权限的文件。

你也可以使用newOutputStream方法创建一个新文件,如使用流 I/O 创建和写入文件中所述。如果你打开一个新的输出流并立即关闭它,将会创建一个空文件。

创建临时文件

你可以使用以下createTempFile方法之一创建一个临时文件:

  • createTempFile(Path, String, String, FileAttribute<?>)

  • createTempFile(String, String, FileAttribute<?>)

第一种方法允许代码指定一个临时文件的目录,而第二种方法则在默认临时文件目录中创建一个新文件。这两种方法都允许你为文件名指定后缀,而第一种方法还允许你指定前缀。以下代码片段给出了第二种方法的示例:

try {
    Path tempFile = Files.createTempFile(null, ".myapp");
    System.out.format("The temporary file" +
        " has been created: %s%n", tempFile)
;
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}

运行此文件的结果将类似于以下内容:

The temporary file has been created: /tmp/509668702974537184.myapp

临时文件名的具体格式取决于平台。

随机访问文件

原文:docs.oracle.com/javase/tutorial/essential/io/rafs.html

随机访问文件允许对文件内容进行非顺序或随机访问。要随机访问文件,您需要打开文件,寻找特定位置,并从该位置读取或写入文件。

这种功能是通过SeekableByteChannel接口实现的。SeekableByteChannel接口扩展了通道 I/O 的概念,具有当前位置的概念。方法使您能够设置或查询位置,然后可以从该位置读取数据或将数据写入该位置。API 由一些易于使用的方法组成:

  • position – 返回通道的当前位置

  • position(long) – 设置通道的位置

  • read(ByteBuffer) – 从通道读取字节到缓冲区

  • write(ByteBuffer) – 将缓冲区中的字节写入通道

  • truncate(long) – 截断与通道连接的文件(或其他实体)

使用通道 I/O 读写文件 显示Path.newByteChannel方法返回SeekableByteChannel的实例。在默认文件系统上,您可以直接使用该通道,或者将其转换为FileChannel,从而可以访问更高级的功能,例如将文件的某个区域直接映射到内存以实现更快的访问,锁定文件的某个区域,或者从绝对位置读取和写入字节而不影响通道的当前位置。

以下代码片段通过使用newByteChannel方法打开文件进行读写。返回的SeekableByteChannel被转换为FileChannel。然后,从文件开头读取 12 个字节,并在该位置写入字符串"I was here!"。文件中的当前位置移动到末尾,并将开头的 12 个字节追加。最后,追加字符串"I was here!",并关闭文件上的通道。

String s = "I was here!\n";
byte data[] = s.getBytes();
ByteBuffer out = ByteBuffer.wrap(data);

ByteBuffer copy = ByteBuffer.allocate(12);

try (FileChannel fc = (FileChannel.open(file, READ, WRITE))) {
    // Read the first 12
    // bytes of the file.
    int nread;
    do {
        nread = fc.read(copy);
    } while (nread != -1 && copy.hasRemaining());

    // Write "I was here!" at the beginning of the file.
    fc.position(0);
    while (out.hasRemaining())
        fc.write(out);
    out.rewind();

    // Move to the end of the file.  Copy the first 12 bytes to
    // the end of the file.  Then write "I was here!" again.
    long length = fc.size();
    fc.position(length-1);
    copy.flip();
    while (copy.hasRemaining())
        fc.write(copy);
    while (out.hasRemaining())
        fc.write(out);
} catch (IOException x) {
    System.out.println("I/O Exception: " + x);
}

创建和读取目录

原文:docs.oracle.com/javase/tutorial/essential/io/dirs.html

其中一些先前讨论过的方法,如delete,适用于文件、链接目录。但是如何列出文件系统顶部的所有目录?如何列出目录的内容或创建目录?

本节涵盖了以下特定于目录的功能:

  • 列出文件系统的根目录

  • 创建目录

  • 创建临时目录

  • 列出目录的内容

  • 通过使用 Globbing 筛选目录列表

  • 编写自己的目录过滤器

列出文件系统的根目录

您可以使用FileSystem.getRootDirectories方法列出文件系统的所有根目录。此方法返回一个Iterable,使您可以使用增强型 for 语句遍历所有根目录。

以下代码片段打印默认文件系统的根目录:

Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories();
for (Path name: dirs) {
    System.err.println(name);
}

创建目录

您可以使用createDirectory(Path, FileAttribute<?>)方法创建一个新目录。如果不指定任何FileAttributes,新目录将具有默认属性。例如:

Path dir = ...;
Files.createDirectory(path);

以下代码片段在具有特定权限的 POSIX 文件系统上创建一个新目录:

Set<PosixFilePermission> perms =
    PosixFilePermissions.fromString("rwxr-x---");
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(perms);
Files.createDirectory(file, attr);

要在可能尚不存在一个或多个父目录的情况下创建几层深的目录,您可以使用方便的方法createDirectories(Path, FileAttribute<?>)。与createDirectory(Path, FileAttribute<?>)方法一样,您可以指定一组可选的初始文件属性。以下代码片段使用默认属性:

Files.createDirectories(Paths.get("foo/bar/test"));

目录是按需自上而下创建的。在foo/bar/test示例中,如果foo目录不存在,则会创建它。接下来,如果需要,将创建bar目录,最后创建test目录。

在创建一些但不是所有父目录后,此方法可能会失败。

创建临时目录

你可以使用createTempDirectory方法之一创建临时目录:

  • createTempDirectory(Path, String, FileAttribute<?>)

  • createTempDirectory(String, FileAttribute<?>...)

第一个方法允许代码指定临时目录的位置,第二个方法在默认临时文件目录中创建一个新目录。

列出目录的内容

您可以使用newDirectoryStream(Path)方法列出目录的所有内容。此方法返回一个实现了DirectoryStream接口的对象。实现DirectoryStream接口的类还实现了Iterable,因此您可以遍历目录流,读取所有对象。这种方法适用于非常大的目录。


记住: 返回的DirectoryStream是一个。如果你没有使用try-with-resources 语句,请不要忘记在finally块中关闭流。try-with-resources 语句会为您处理这个问题。


以下代码片段展示了如何打印目录的内容:

Path dir = ...;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
    for (Path file: stream) {
        System.out.println(file.getFileName());
    }
} catch (IOException | DirectoryIteratorException x) {
    // IOException can never be thrown by the iteration.
    // In this snippet, it can only be thrown by newDirectoryStream.
    System.err.println(x);
}

迭代器返回的Path对象是相对于目录解析的条目名称。因此,如果您正在列出/tmp目录的内容,则条目将以/tmp/a/tmp/b等形式返回。

此方法返回目录的全部内容:文件、链接、子目录和隐藏文件。如果您想更有选择地检索内容,可以使用本页后面描述的其他newDirectoryStream方法之一。

请注意,如果在目录迭代过程中出现异常,则会抛出DirectoryIteratorException,其原因是IOException。迭代器方法不能抛出异常。

通过使用 Globbing 筛选目录列表

如果您只想获取文件和子目录,其中每个名称都匹配特定模式,可以使用newDirectoryStream(Path, String)方法,该方法提供了内置的 glob 过滤器。如果您不熟悉 glob 语法,请参阅什么是 Glob?

例如,以下代码片段列出了与 Java 相关的文件:.class.java.jar文件。

Path dir = ...;
try (DirectoryStream<Path> stream =
     Files.newDirectoryStream(dir, "*.{java,class,jar}")) {
    for (Path entry: stream) {
        System.out.println(entry.getFileName());
    }
} catch (IOException x) {
    // IOException can never be thrown by the iteration.
    // In this snippet, it can // only be thrown by newDirectoryStream.
    System.err.println(x);
}

编写自己的目录过滤器

也许您想要根据除了模式匹配之外的某些条件来过滤目录的内容。您可以通过实现DirectoryStream.Filter<T>接口来创建自己的过滤器。该接口由一个方法accept组成,用于确定文件是否满足搜索要求。

例如,以下代码片段实现了一个仅检索目录的过滤器:

DirectoryStream.Filter<Path> filter =
    newDirectoryStream.Filter<Path>() {
    public boolean accept(Path file) throws IOException {
        try {
            return (Files.isDirectory(path));
        } catch (IOException x) {
            // Failed to determine if it's a directory.
            System.err.println(x);
            return false;
        }
    }
};

一旦过滤器被创建,就可以通过使用newDirectoryStream(Path, DirectoryStream.Filter<? super Path>)方法来调用它。以下代码片段使用isDirectory过滤器仅将目录的子目录打印到标准输出:

Path dir = ...;
try (DirectoryStream<Path>
                       stream = Files.newDirectoryStream(dir, filter)) {
    for (Path entry: stream) {
        System.out.println(entry.getFileName());
    }
} catch (IOException x) {
    System.err.println(x);
}

此方法仅用于过滤单个目录。然而,如果您想要在文件树中找到所有子目录,您将使用遍历文件树的机制。

链接,符号或其他

原文:docs.oracle.com/javase/tutorial/essential/io/links.html

如前所述,java.nio.file包,特别是Path类是“链接感知”的。每个Path方法都会检测遇到符号链接时该做什么,或者提供一个选项,使您能够配置遇到符号链接时的行为。

到目前为止的讨论一直是关于符号或链接,但一些文件系统也支持硬链接。硬链接比符号链接更受限制,具体如下:

  • 链接的目标必须存在。

  • 通常不允许在目录上创建硬链接。

  • 硬链接不允许跨分区或卷。因此,它们不能存在于不同文件系统之间。

  • 一个硬链接看起来和行为都像一个普通文件,所以它们可能很难找到。

  • 从所有方面来看,硬链接与原始文件是相同的实体。它们具有相同的文件权限、时间戳等。所有属性都是相同的。

由于这些限制,硬链接不像符号链接那样经常使用,但Path方法与硬链接无缝配合。

有几种方法专门处理链接,并在以下部分中介绍:

  • 创建符号链接

  • 创建硬链接

  • 检测符号链接

  • 查找链接的目标

创建符号链接

如果你的文件系统支持,你可以使用createSymbolicLink(Path, Path, FileAttribute<?>)方法创建一个符号链接。第二个Path参数表示目标文件或目录,可能存在也可能不存在。以下代码片段创建了一个带有默认权限的符号链接:

Path newLink = ...;
Path target = ...;
try {
    Files.createSymbolicLink(newLink, target);
} catch (IOException x) {
    System.err.println(x);
} catch (UnsupportedOperationException x) {
    // Some file systems do not support symbolic links.
    System.err.println(x);
}

FileAttributes vararg 使您能够指定在创建链接时原子设置的初始文件属性。但是,这个参数是为将来使用而设计的,目前尚未实现。

创建硬链接

你可以使用createLink(Path, Path)方法创建一个到现有文件的硬(或常规)链接。第二个Path参数定位现有文件,它必须存在,否则会抛出NoSuchFileException。以下代码片段展示了如何创建链接:

Path newLink = ...;
Path existingFile = ...;
try {
    Files.createLink(newLink, existingFile);
} catch (IOException x) {
    System.err.println(x);
} catch (UnsupportedOperationException x) {
    // Some file systems do not
    // support adding an existing
    // file to a directory.
    System.err.println(x);
}

检测符号链接

要确定Path实例是否是符号链接,可以使用isSymbolicLink(Path)方法。以下代码片段展示了如何:

Path file = ...;
boolean isSymbolicLink =
    Files.isSymbolicLink(file);

欲了解更多信息,请参阅管理元数据。

查找链接的目标

通过使用readSymbolicLink(Path)方法,您可以获取符号链接的目标,如下所示:

Path link = ...;
try {
    System.out.format("Target of link" +
        " '%s' is '%s'%n", link,
        Files.readSymbolicLink(link));
} catch (IOException x) {
    System.err.println(x);
}

如果Path不是一个符号链接,该方法会抛出NotLinkException

遍历文件树

原文:docs.oracle.com/javase/tutorial/essential/io/walk.html

您是否需要创建一个应用程序,递归访问文件树中的所有文件?也许您需要删除树中的每个.class文件,或者找到在过去一年中未被访问的每个文件。您可以通过FileVisitor接口实现这一点。

本节涵盖以下内容:

  • FileVisitor 接口

  • 启动过程

  • 创建 FileVisitor 时的注意事项

  • 控制流程

  • 示例

FileVisitor 接口

要遍历文件树,首先需要实现一个FileVisitorFileVisitor指定了在遍历过程的关键点上所需的行为:当访问文件时,在访问目录之前,在访问目录之后,或者当发生故障时。该接口有四个方法对应于这些情况:

  • preVisitDirectory – 在访问目录条目之前调用。

  • postVisitDirectory – 在访问目录中的所有条目之后调用。如果遇到任何错误,特定异常将传递给该方法。

  • visitFile – 在访问文件时调用。文件的BasicFileAttributes被传递给该方法,或者您可以使用 file attributes 包来读取特定的属性集。例如,您可以选择读取文件的DosFileAttributeView来确定文件是否设置了“hidden”位。

  • visitFileFailed – 当无法访问文件时调用。特定异常被传递给该方法。您可以选择是否抛出异常,将其打印到控制台或日志文件等。

如果您不需要实现所有四个FileVisitor方法,而是扩展SimpleFileVisitor类,而不是实现FileVisitor接口。这个类实现了FileVisitor接口,访问树中的所有文件,并在遇到错误时抛出IOError。您可以扩展这个类,并仅覆盖您需要的方法。

这是一个扩展SimpleFileVisitor以打印文件树中所有条目的示例。它打印条目,无论条目是常规文件、符号链接、目录还是其他类型的“未指定”文件。它还打印每个文件的字节大小。遇到的任何异常都会打印到控制台。

FileVisitor方法以粗体显示:

import static java.nio.file.FileVisitResult.*;

public static class PrintFiles
    extends SimpleFileVisitor<Path> {

    // Print information about
    // each type of file.
    @Override
    public FileVisitResult visitFile(Path file,
                                   BasicFileAttributes attr) {
        if (attr.isSymbolicLink()) {
            System.out.format("Symbolic link: %s ", file);
        } else if (attr.isRegularFile()) {
            System.out.format("Regular file: %s ", file);
        } else {
            System.out.format("Other: %s ", file);
        }
        System.out.println("(" + attr.size() + "bytes)");
        return CONTINUE;
    }

    // Print each directory visited.
    @Override
    public FileVisitResult postVisitDirectory(Path dir,
                                          IOException exc) {
        System.out.format("Directory: %s%n", dir);
        return CONTINUE;
    }

    // If there is some error accessing
    // the file, let the user know.
    // If you don't override this method
    // and an error occurs, an IOException 
    // is thrown.
    @Override
    public FileVisitResult visitFileFailed(Path file,
                                       IOException exc) {
        System.err.println(exc);
        return CONTINUE;
    }
}

启动过程

一旦您实现了您的FileVisitor,如何启动文件遍历?Files类中有两个walkFileTree方法。

  • walkFileTree(Path, FileVisitor)

  • walkFileTree(Path, Set<FileVisitOption>, int, FileVisitor)

第一个方法只需要一个起始点和您的FileVisitor的实例。您可以按以下方式调用PrintFiles文件访问者:

Path startingDir = ...;
PrintFiles pf = new PrintFiles();
Files.walkFileTree(startingDir, pf);

第二个walkFileTree方法还允许您额外指定访问级别的限制和一组FileVisitOption枚举。如果您希望确保此方法遍历整个文件树,您可以为最大深度参数指定Integer.MAX_VALUE

您可以指定FileVisitOption枚举FOLLOW_LINKS,表示应该跟随符号链接。

此代码片段显示了如何调用四参数方法:

import static java.nio.file.FileVisitResult.*;

Path startingDir = ...;

EnumSet<FileVisitOption> opts = EnumSet.of(FOLLOW_LINKS);

Finder finder = new Finder(pattern);
Files.walkFileTree(startingDir, opts, Integer.MAX_VALUE, finder);

创建FileVisitor时的注意事项

文件树以深度优先方式遍历,但不能假设子目录的访问顺序。

如果您的程序将更改文件系统,您需要仔细考虑如何实现您的FileVisitor

例如,如果您正在编写递归删除,您首先删除目录中的文件,然后再删除目录本身。在这种情况下,您在postVisitDirectory中删除目录。

如果您正在编写递归复制,您需要在preVisitDirectory中创建新目录,然后尝试将文件复制到其中(在visitFiles中)。如果您想要保留源目录的属性(类似于 UNIX 的cp -p命令),您需要在文件被复制后,在postVisitDirectory中执行此操作。Copy示例展示了如何做到这一点。

如果您正在编写文件搜索,您可以在visitFile方法中执行比较。此方法找到所有符合您条件的文件,但不会找到目录。如果您想要找到文件和目录,您还必须在preVisitDirectorypostVisitDirectory方法中执行比较。Find示例展示了如何做到这一点。

你需要决定是否要遵循符号链接。例如,如果你正在删除文件,跟随符号链接可能不明智。如果你正在复制文件树,你可能希望允许它。默认情况下,walkFileTree不会遵循符号链接。

对于文件,会调用visitFile方法。如果你指定了FOLLOW_LINKS选项,并且你的文件树有一个指向父目录的循环链接,循环目录将在visitFileFailed方法中报告,带有FileSystemLoopException。以下代码片段显示了如何捕获循环链接,并来自于Copy示例:

@Override
public FileVisitResult
    visitFileFailed(Path file,
        IOException exc) {
    if (exc instanceof FileSystemLoopException) {
        System.err.println("cycle detected: " + file);
    } else {
        System.err.format("Unable to copy:" + " %s: %s%n", file, exc);
    }
    return CONTINUE;
}

这种情况只会在程序遵循符号链接时发生。

控制流程

也许你想要遍历文件树查找特定目录,并且在找到后希望进程终止。也许你想要跳过特定目录。

FileVisitor方法返回一个FileVisitResult值。你可以通过在FileVisitor方法中返回的值来中止文件遍历过程或控制是否访问目录:

  • CONTINUE – 表示文件遍历应该继续。如果preVisitDirectory方法返回CONTINUE,则会访问该目录。

  • TERMINATE – 立即中止文件遍历。在返回此值后不会调用更多的文件遍历方法。

  • SKIP_SUBTREE – 当preVisitDirectory返回此值时,指定的目录及其子目录将被跳过。这个分支将从树中“剪掉”。

  • SKIP_SIBLINGS – 当preVisitDirectory返回此值时,指定的目录不会被访问,postVisitDirectory不会被调用,也不会访问更多未访问的兄弟节点。如果从postVisitDirectory方法返回,不会访问更多的兄弟节点。基本上,在指定的目录中不会发生更多的事情。

在这段代码片段中,任何名为SCCS的目录都会被跳过:

import static java.nio.file.FileVisitResult.*;

public FileVisitResult
     preVisitDirectory(Path dir,
         BasicFileAttributes attrs) {
    (if (dir.getFileName().toString().equals("SCCS")) {
         return SKIP_SUBTREE;
    }
    return CONTINUE;
}

在这段代码片段中,一旦找到特定文件,文件名就会被打印到标准输出,并且文件遍历会终止:

import static java.nio.file.FileVisitResult.*;

// The file we are looking for.
Path lookingFor = ...;

public FileVisitResult
    visitFile(Path file,
        BasicFileAttributes attr) {
    if (file.getFileName().equals(lookingFor)) {
        System.out.println("Located file: " + file);
        return TERMINATE;
    }
    return CONTINUE;
}

示例

以下示例演示了文件遍历机制:

  • Find – 递归查找符合特定通配符模式的文件和目录。此示例在查找文件中讨论。

  • Chmod – 递归更改文件树上的权限(仅适用于 POSIX 系统)。

  • Copy – 递归复制文件树。

  • WatchDir – 演示了监视目录中已创建、删除或修改的文件的机制。使用-r选项调用此程序会监视整个树的更改。有关文件通知服务的更多信息,请参见监视目录的更改。

查找文件

原文:docs.oracle.com/javase/tutorial/essential/io/find.html

如果你曾经使用过 shell 脚本,你很可能使用过模式匹配来定位文件。事实上,你可能已经广泛使用了它。如果你还没有使用过,模式匹配使用特殊字符创建模式,然后文件名可以与该模式进行比较。例如,在大多数 shell 脚本中,星号,*,匹配任意数量的字符。例如,以下命令列出当前目录中以.html结尾的所有文件:

% ls *.html

java.nio.file包为这一有用功能提供了编程支持。每个文件系统实现都提供了一个PathMatcher。你可以通过在FileSystem类中使用getPathMatcher(String)方法来检索文件系统的PathMatcher。以下代码片段获取默认文件系统的路径匹配器:

String pattern = ...;
PathMatcher matcher =
    FileSystems.getDefault().getPathMatcher("glob:" + pattern);

传递给getPathMatcher的字符串参数指定语法风格和要匹配的模式。本示例指定了glob语法。如果你不熟悉 glob 语法,请参阅什么是 Glob。

Glob 语法易于使用和灵活,但如果你喜欢,也可以使用正则表达式,或regex语法。有关正则表达式的更多信息,请参阅正则表达式课程。一些文件系统实现可能支持其他语法。

如果你想使用其他形式的基于字符串的模式匹配,你可以创建自己的PathMatcher类。本页中的示例使用 glob 语法。

一旦你创建了PathMatcher实例,你就可以准备好根据它匹配文件。PathMatcher接口有一个方法,matches,它接受一个Path参数并返回一个布尔值:它要么匹配模式,要么不匹配。以下代码片段查找以.java.class结尾的文件并将这些文件打印到标准输出:

PathMatcher matcher =
    FileSystems.getDefault().getPathMatcher("glob:*.{java,class}");

Path filename = ...;
if (matcher.matches(filename)) {
    System.out.println(filename);
}

递归模式匹配

搜索与特定模式匹配的文件与遍历文件树密切相关。有多少次你知道一个文件在某处在文件系统上,但在哪里?或者也许你需要找到文件树中具有特定文件扩展名的所有文件。

Find示例正是如此。Find类似于 UNIX 的find实用程序,但功能更简化。你可以扩展这个示例以包含其他功能。例如,find实用程序支持-prune标志来排除搜索中的整个子树。你可以通过在preVisitDirectory方法中返回SKIP_SUBTREE来实现该功能。要实现-L选项,即跟随符号链接,你可以使用四个参数的walkFileTree方法,并传入FOLLOW_LINKS枚举(但请确保在visitFile方法中测试循环链接)。

要运行 Find 应用程序,请使用以下格式:

% java Find <path> -name "<glob_pattern>"

模式被放置在引号内,以防止 shell 解释任何通配符。例如:

% java Find . -name "*.html"

这里是Find示例的源代码:

/**
 * Sample code that finds files that match the specified glob pattern.
 * For more information on what constitutes a glob pattern, see
 * https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob
 *
 * The file or directories that match the pattern are printed to
 * standard out.  The number of matches is also printed.
 *
 * When executing this application, you must put the glob pattern
 * in quotes, so the shell will not expand any wild cards:
 *              java Find . -name "*.java"
 */

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import static java.nio.file.FileVisitResult.*;
import static java.nio.file.FileVisitOption.*;
import java.util.*;

public class Find {

    public static class Finder
        extends SimpleFileVisitor<Path> {

        private final PathMatcher matcher;
        private int numMatches = 0;

        Finder(String pattern) {
            matcher = FileSystems.getDefault()
                    .getPathMatcher("glob:" + pattern);
        }

        // Compares the glob pattern against
        // the file or directory name.
        void find(Path file) {
            Path name = file.getFileName();
            if (name != null && matcher.matches(name)) {
                numMatches++;
                System.out.println(file);
            }
        }

        // Prints the total number of
        // matches to standard out.
        void done() {
            System.out.println("Matched: "
                + numMatches);
        }

        // Invoke the pattern matching
        // method on each file.
        @Override
        public FileVisitResult visitFile(Path file,
                BasicFileAttributes attrs) {
            find(file);
            return CONTINUE;
        }

        // Invoke the pattern matching
        // method on each directory.
        @Override
        public FileVisitResult preVisitDirectory(Path dir,
                BasicFileAttributes attrs) {
            find(dir);
            return CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path file,
                IOException exc) {
            System.err.println(exc);
            return CONTINUE;
        }
    }

    static void usage() {
        System.err.println("java Find <path>" +
            " -name \"<glob_pattern>\"");
        System.exit(-1);
    }

    public static void main(String[] args)
        throws IOException {

        if (args.length < 3 || !args[1].equals("-name"))
            usage();

        Path startingDir = Paths.get(args[0]);
        String pattern = args[2];

        Finder finder = new Finder(pattern);
        Files.walkFileTree(startingDir, finder);
        finder.done();
    }
}

递归遍历文件树的内容在遍历文件树中有详细介绍。

监视目录更改

原文:docs.oracle.com/javase/tutorial/essential/io/notification.html

你是否曾经发现自己正在编辑一个文件,使用 IDE 或另一个编辑器,并且出现一个对话框通知您文件系统中的一个打开文件已更改并需要重新加载?或者,就像 NetBeans IDE 一样,应用程序悄悄地更新文件而不通知您。以下示例对话框显示了使用免费编辑器jEdit时的通知外观:

示例 jEdit 对话框显示:以下文件已被另一个程序更改。jEdit 对话框显示检测到修改的文件

要实现此功能,称为文件更改通知,程序必须能够检测到文件系统上相关目录发生的变化。一种方法是轮询文件系统以查找更改,但这种方法效率低下。它不适用于具有数百个打开文件或目录需要监视的应用程序。

java.nio.file包提供了一个文件更改通知 API,称为 Watch Service API。此 API 使您能够向观察服务注册目录(或目录)。在注册时,您告诉服务您感兴趣的事件类型:文件创建、文件删除或文件修改。当服务检测到感兴趣的事件时,它会转发给注册的进程。注册的进程有一个专用于监视其注册事件的线程(或线程池)。当事件发生时,根据需要进行处理。

本节涵盖以下内容:

  • 观察服务概述

  • 试一试

  • 创建 Watch Service 并注册事件

  • 处理事件

  • 获取文件名

  • 何时使用和不使用此 API

观察服务概述

WatchService API 相当低级,允许您自定义它。您可以直接使用它,或者您可以选择在此机制之上创建一个高级 API,以使其适合您的特定需求。

下面是实现观察服务所需的基本步骤:

  • 为文件系统创建一个WatchService“观察者”。

  • 对于要监视的每个目录,请将其注册到观察者中。在注册目录时,指定要接收通知的事件类型。您为每个注册的目录收到一个WatchKey实例。

  • 实现一个无限循环以等待传入事件。当事件发生时,键被标记并放入观察者队列中。

  • 从观察者队列中检索键。您可以从键中获取文件名。

  • 检索键的每个待处理事件(可能有多个事件)并根据需要处理。

  • 重置键,并恢复等待事件。

  • 关闭服务:当线程退出或调用其closed方法关闭服务时,监视服务将退出。

WatchKeys是线程安全的,可以与java.nio.concurrent包一起使用。您可以为此目的专门分配一个线程池。

试一试

由于此 API 更为高级,请在继续之前先尝试一下。将WatchDir示例保存到您的计算机上,并对其进行编译。创建一个将传递给示例的test目录。WatchDir使用单个线程处理所有事件,因此在等待事件时会阻止键盘输入。要么在单独的窗口中运行程序,要么在后台运行,如下所示:

java WatchDir test &

test目录中创建、删除和编辑文件。当发生任何这些事件时,将在控制台上打印消息。完成后,删除test目录,WatchDir退出。或者,如果您愿意,也可以手动终止进程。

您还可以通过指定-r选项来监视整个文件树。当您指定-r时,WatchDir遍历文件树,将每个目录注册到监视服务中。

创建监视服务并注册事件

第一步是通过FileSystem类中的newWatchService方法创建一个新的WatchService,如下所示:

WatchService watcher = FileSystems.getDefault().newWatchService();

接下来,向监视服务注册一个或多个对象。任何实现了Watchable接口的对象都可以注册。Path类实现了Watchable接口,因此要监视的每个目录都被注册为一个Path对象。

与任何Watchable一样,Path类实现了两个register方法。本页使用了两个参数版本的register(WatchService, WatchEvent.Kind<?>...)。(三个参数版本接受一个WatchEvent.Modifier,目前尚未实现。)

在向监视服务注册对象时,您需要指定要监视的事件类型。支持的StandardWatchEventKinds事件类型如下:

  • ENTRY_CREATE – 创建目录条目。

  • ENTRY_DELETE – 删除目录条目。

  • ENTRY_MODIFY – 修改目录条目。

  • OVERFLOW – 表示事件可能已丢失或被丢弃。您无需注册OVERFLOW事件即可接收它。

以下代码片段显示了如何为所有三种事件类型注册Path实例:

import static java.nio.file.StandardWatchEventKinds.*;

Path dir = ...;
try {
    WatchKey key = dir.register(watcher,
                           ENTRY_CREATE,
                           ENTRY_DELETE,
                           ENTRY_MODIFY);
} catch (IOException x) {
    System.err.println(x);
}

处理事件

事件处理循环中事件的顺序如下:

  1. 获取一个监视键。提供了三种方法:

    • poll – 如果可用,则返回一个排队的键。如果不可用,则立即返回null值。

    • poll(long, TimeUnit) – 如果有排队的键可用,则返回一个。如果没有立即可用的排队键,则程序将等待指定的时间。TimeUnit参数确定指定的时间是纳秒、毫秒还是其他时间单位。

    • take – 返回一个排队的键。如果没有可用的排队键,此方法将等待。

  2. 处理键的待处理事件。您从pollEvents方法中获取WatchEventsList

  3. 使用kind方法检索事件的类型。无论键注册了什么事件,都有可能收到OVERFLOW事件。您可以选择处理溢出或忽略它,但应该对其进行测试。

  4. 检索与事件关联的文件名。文件名存储为事件的上下文,因此使用context方法来检索它。

  5. 处理键的事件后,需要通过调用reset将键放回ready状态。如果此方法返回false,则键不再有效,循环可以退出。这一步非常重要。如果未调用reset,则此键将不会接收到进一步的事件。

观察键具有状态。在任何给定时间,其状态可能是以下之一:

  • Ready表示键已准备好接受事件。创建时,键处于准备状态。

  • Signaled表示有一个或多个事件排队。一旦键被标记,它就不再处于准备状态,直到调用reset方法。

  • Invalid表示键不再活动。当发生以下事件之一时,会出现此状态:

    • 进程通过使用cancel方法显式取消键。

    • 目录变得无法访问。

    • 观察服务已经被关闭。

这里是一个事件处理循环的示例。它取自于 Email 示例,该示例监视一个目录,等待新文件出现。当新文件可用时,通过使用 probeContentType(Path) 方法来检查它是否是一个 text/plain 文件。意图是将 text/plain 文件发送到一个别名,但具体实现细节留给读者。

Watch service API 特定的方法用粗体显示:

for (;;) {

    // wait for key to be signaled
    WatchKey key;
    try {
        key = watcher.take();
    } catch (InterruptedException x) {
        return;
    }

    for (WatchEvent<?> event: key.pollEvents()) {
        WatchEvent.Kind<?> kind = event.kind();

        // This key is registered only
        // for ENTRY_CREATE events,
        // but an OVERFLOW event can
        // occur regardless if events
        // are lost or discarded.
        if (kind == OVERFLOW) {
            continue;
        }

        // The filename is the
        // context of the event.
        WatchEvent<Path> ev = (WatchEvent<Path>)event;
        Path filename = ev.context();

        // Verify that the new
        //  file is a text file.
        try {
            // Resolve the filename against the directory.
            // If the filename is "test" and the directory is "foo",
            // the resolved name is "test/foo".
            Path child = dir.resolve(filename);
            if (!Files.probeContentType(child).equals("text/plain")) {
                System.err.format("New file '%s'" +
                    " is not a plain text file.%n", filename);
                continue;
            }
        } catch (IOException x) {
            System.err.println(x);
            continue;
        }

        // Email the file to the
        //  specified email alias.
        System.out.format("Emailing file %s%n", filename);
        //Details left to reader....
    }

    // Reset the key -- this step is critical if you want to
    // receive further watch events.  If the key is no longer valid,
    // the directory is inaccessible so exit the loop.
    boolean valid = key.reset();
    if (!valid) {
        break;
    }
}

检索文件名

文件名是从事件上下文中检索的。Email 示例使用以下代码检索文件名:

WatchEvent<Path> ev = (WatchEvent<Path>)event;
Path filename = ev.context();

当你编译 Email 示例时,会生成以下错误:

Note: Email.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

这个错误是由将 WatchEvent<T> 强制转换为 WatchEvent<Path> 的代码行引起的。WatchDir 示例通过创建一个抑制未经检查警告的实用 cast 方法来避免这个错误,如下所示:

@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
    return (WatchEvent<Path>)event;
}

如果你对 @SuppressWarnings 语法不熟悉,请参见 Annotations。

何时使用和不使用这个 API

Watch Service API 适用于需要通知文件更改事件的应用程序。它非常适合任何可能有许多打开文件并需要确保文件与文件系统同步的应用程序,比如编辑器或 IDE。它也非常适合监视目录的应用服务器,也许等待 .jsp.jar 文件的出现,以便部署它们。

这个 API 是为了索引硬盘而设计的。大多数文件系统实现都原生支持文件更改通知。Watch Service API 利用了这种支持(如果可用)。然而,当文件系统不支持这种机制时,Watch Service 将轮询文件系统,等待事件发生。

其他有用的方法

原文:docs.oracle.com/javase/tutorial/essential/io/misc.html

本课程中未涵盖的一些有用方法在此处介绍。本节涵盖以下内容:

  • 确定 MIME 类型

  • 默认文件系统

  • 路径字符串分隔符

  • 文件系统的文件存储器

确定 MIME 类型

要确定文件的 MIME 类型,您可能会发现probeContentType(Path)方法很有用。例如:

try {
    String type = Files.probeContentType(filename);
    if (type == null) {
        System.err.format("'%s' has an" + " unknown filetype.%n", filename);
    } else if (!type.equals("text/plain") {
        System.err.format("'%s' is not" + " a plain text file.%n", filename);
        continue;
    }
} catch (IOException x) {
    System.err.println(x);
}

注意,如果无法确定内容类型,probeContentType会返回 null。

此方法的实现高度依赖于平台,并不是绝对可靠的。内容类型由平台的默认文件类型检测器确定。例如,如果检测器根据.class扩展名确定文件的内容类型为application/x-java,可能会被欺骗。

如果默认的方法不符合您的需求,您可以提供自定义的FileTypeDetector

电子邮件示例使用probeContentType方法。

默认文件系统

要检索默认文件系统,请使用getDefault方法。通常,此FileSystems方法(注意是复数形式)链接到FileSystem方法之一(注意是单数形式),如下所示:

PathMatcher matcher =
    FileSystems.getDefault().getPathMatcher("glob:*.*");

路径字符串分隔符

POSIX 文件系统的路径分隔符是正斜杠/,Microsoft Windows 的路径分隔符是反斜杠\。其他文件系统可能使用其他分隔符。要检索默认文件系统的Path分隔符,可以使用以下方法之一:

String separator = File.separator;
String separator = FileSystems.getDefault().getSeparator();

getSeparator方法也用于检索任何可用文件系统的路径分隔符。

文件系统的文件存储器

文件系统有一个或多个文件存储器来保存其文件和目录。文件存储器代表底层存储设备。在 UNIX 操作系统中,每个挂载的文件系统都由一个文件存储器表示。在 Microsoft Windows 中,每个卷都由一个文件存储器表示:C:D:等等。

要检索文件系统的所有文件存储器列表,可以使用getFileStores方法。此方法返回一个Iterable,允许您使用增强的 for 语句遍历所有根目录。

for (FileStore store: FileSystems.getDefault().getFileStores()) {
   ...
}

如果要检索特定文件所在的文件存储器,请使用Files类中的getFileStore方法,如下所示:

Path file = ...;
FileStore store= Files.getFileStore(file);

DiskUsage示例使用getFileStores方法。

传统文件 I/O 代码

原文:docs.oracle.com/javase/tutorial/essential/io/legacy.html

与旧代码的互操作性

在 Java SE 7 发布之前,java.io.File 类是文件 I/O 的机制,但它有一些缺点。

  • 许多方法在失败时不会抛出异常,因此无法获得有用的错误消息。例如,如果文件删除失败,程序将收到“删除失败”,但不知道是因为文件不存在、用户没有权限还是其他问题。

  • rename 方法在各个平台上的工作不一致。

  • 没有对符号链接的真正支持。

  • 需要更多对元数据的支持,如文件权限、文件所有者和其他安全属性。

  • 访问文件元数据效率低下。

  • 许多 File 方法不具备可扩展性。在服务器上请求大型目录列表可能导致挂起。大型目录也可能导致内存资源问题,导致拒绝服务。

  • 不可能编写可靠的代码,可以递归遍历文件树,并在存在循环符号链接时做出适当响应。

也许您有使用 java.io.File 的旧代码,并希望最小影响地利用 java.nio.file.Path 功能。

java.io.File 类提供了 toPath 方法,将旧式 File 实例转换为 java.nio.file.Path 实例,如下所示:

Path input = file.toPath();

然后,您可以利用 Path 类提供的丰富功能集。

例如,假设您有一些删除文件的代码:

file.delete();

您可以修改此代码以使用 Files.delete 方法,如下所示:

Path fp = file.toPath();
Files.delete(fp);

相反,Path.toFile 方法为 Path 对象构造一个 java.io.File 对象。

将 java.io.File 功能映射到 java.nio.file

由于 Java SE 7 发布中的文件 I/O 实现已完全重新架构,因此不能将一个方法替换为另一个方法。如果您想使用 java.nio.file 包提供的丰富功能,最简单的解决方案是使用前一节中建议的 File.toPath 方法。但是,如果您不想使用该方法或该方法不符合您的需求,您必须重写文件 I/O 代码。

两个 API 之间没有一对一对应关系,但以下表格给出了 java.io.File API 中的功能在 java.nio.file API 中的映射,并告诉您可以在哪里获取更多信息。

java.io.File 功能 java.nio.file 功能 教程覆盖范围
java.io.File java.nio.file.Path Path 类
java.io.RandomAccessFile SeekableByteChannel 功能。 随机访问文件
File.canReadcanWritecanExecute Files.isReadableFiles.isWritableFiles.isExecutable。在 UNIX 文件系统上,使用 管理元数据(文件和文件存储属性) 包来检查九个文件权限。 检查文件或目录 管理元数据
File.isDirectory()File.isFile()File.length() Files.isDirectory(Path, LinkOption...)Files.isRegularFile(Path, LinkOption...)Files.size(Path) 管理元数据
File.lastModified()File.setLastModified(long) Files.getLastModifiedTime(Path, LinkOption...)Files.setLastMOdifiedTime(Path, FileTime) 管理元数据
设置各种属性的 File 方法:setExecutablesetReadablesetReadOnlysetWritable 这些方法被 Files 方法 setAttribute(Path, String, Object, LinkOption...) 替代。 管理元数据
new File(parent, "newfile") parent.resolve("newfile") 路径操作
File.renameTo Files.move 移动文件或目录
File.delete Files.delete 删除文件或目录
File.createNewFile Files.createFile 创建文件
File.deleteOnExit createFile 方法中指定的 DELETE_ON_CLOSE 选项替代。 创建文件

| File.createTempFile | Files.createTempFile(Path, String, FileAttributes<?>)Files.createTempFile(Path, String, String, FileAttributes<?>) | 创建文件 通过流 I/O 创建和写入文件

通过通道 I/O 读写文件 |

File.exists Files.existsFiles.notExists 验证文件或目录的存在性
File.compareTo and equals Path.compareTo and equals 比较两个路径
File.getAbsolutePath and getAbsoluteFile Path.toAbsolutePath 转换路径

| File.getCanonicalPath and getCanonicalFile | Path.toRealPathnormalize | 转换路径 (toRealPath) 从路径中删除冗余部分 (normalize)

|

File.toURI Path.toURI 转换路径
File.isHidden Files.isHidden 检索路径信息
File.list and listFiles Path.newDirectoryStream 列出目录内容
File.mkdirmkdirs Files.createDirectory 创建目录
File.listRoots FileSystem.getRootDirectories 列出文件系统的根目录
File.getTotalSpaceFile.getFreeSpaceFile.getUsableSpace FileStore.getTotalSpaceFileStore.getUnallocatedSpaceFileStore.getUsableSpaceFileStore.getTotalSpace 文件存储属性

摘要

原文:docs.oracle.com/javase/tutorial/essential/io/summary.html

java.io 包包含许多类,您的程序可以使用这些类来读取和写入数据。大多数类实现顺序访问流。顺序访问流可以分为两组:那些读取和写入字节的流以及读取和写入 Unicode 字符的流。每个顺序访问流都有其特长,例如从文件中读取或写入数据,过滤读取或写入的数据,或者序列化对象。

java.nio.file 包提供了广泛的文件和文件系统 I/O 支持。这是一个非常全面的 API,但关键入口点如下:

  • Path 类具有操作路径的方法。

  • Files 类具有文件操作的方法,例如移动、复制、删除,以及检索和设置文件属性的方法。

  • FileSystem 类具有各种方法用于获取有关文件系统的信息。

关于 NIO.2 的更多信息可以在 OpenJDK: NIO 项目网站上找到。该网站包括 NIO.2 提供的超出本教程范围的功能资源,例如多播、异步 I/O 和创建自己的文件系统实现。

问题和练习:基本 I/O

原文:docs.oracle.com/javase/tutorial/essential/io/QandE/questions.html

问题

1. 你会使用什么类和方法来读取大文件末尾附近已知位置的几个数据片段?

2. 在调用format时,如何最好地指示一个新行?

3. 如何确定文件的 MIME 类型?

4. 您会使用什么方法来确定文件是否是符号链接?

练习

1. 编写一个示例,计算文件中特定字符(如e)出现的次数。可以在命令行指定字符。您可以使用xanadu.txt作为输入文件。

2. 文件datafile以一个告诉你同一文件中一个int数据偏移量的long开头。编写一个程序获取这个int数据。这个int数据是什么?

检查你的答案。

课程:并发编程

原文:docs.oracle.com/javase/tutorial/essential/concurrency/index.html

计算机用户认为他们的系统可以同时执行多项任务是理所当然的。他们认为他们可以在一个文字处理器中继续工作,同时其他应用程序可以下载文件,管理打印队列和流式传输音频。甚至单个应用程序通常也被期望同时执行多项任务。例如,流式传输音频应用程序必须同时从网络上读取数字音频,解压缩它,管理播放和更新显示。即使文字处理器也应该始终准备好响应键盘和鼠标事件,无论它是在重新格式化文本还是更新显示。能够执行这些操作的软件被称为并发软件。

Java 平台从头开始就设计用于支持并发编程,在 Java 编程语言和 Java 类库中具有基本的并发支持。自 5.0 版本以来,Java 平台还包括高级并发 API。本课程介绍了平台的基本并发支持,并总结了java.util.concurrent包中的一些高级 API。

进程和线程

原文:docs.oracle.com/javase/tutorial/essential/concurrency/procthread.html

在并发编程中,有两个基本的执行单位:进程线程。在 Java 编程语言中,并发编程主要涉及线程。然而,进程也很重要。

计算机系统通常有许多活动进程和线程。即使在只有一个执行核心的系统中,因此在任何给定时刻只有一个线程实际执行,也是如此。单核心的处理时间通过操作系统的时间片特性在进程和线程之间共享。

现在越来越普遍的是计算机系统具有多个处理器或具有多个执行核心的处理器。这极大地增强了系统对进程和线程并发执行的能力 — 但即使在简单系统上,没有多个处理器或执行核心,也可以实现并发。

进程

一个进程有一个独立的执行环境。一个进程通常有一个完整的,私有的基本运行时资源集;特别是,每个进程都有自己的内存空间。

进程通常被视为与程序或应用程序同义。然而,用户所看到的单个应用程序实际上可能是一组协作的进程。为了促进进程之间的通信,大多数操作系统支持进程间通信(IPC)资源,如管道和套接字。IPC 不仅用于同一系统上进程之间的通信,还用于不同系统上的进程。

大多数 Java 虚拟机的实现作为一个单独的进程运行。Java 应用程序可以使用ProcessBuilder对象创建额外的进程。多进程应用程序超出了本课程的范围。

线程

线程有时被称为轻量级进程。进程和线程都提供执行环境,但创建一个新线程所需的资源比创建一个新进程少。

线程存在于一个进程中 — 每个进程至少有一个。线程共享进程的资源,包括内存和打开的文件。这样做可以实现高效的,但潜在的有问题的通信。

多线程执行是 Java 平台的一个重要特性。每个应用程序至少有一个线程 — 或者多个,如果计算“系统”线程,执行诸如内存管理和信号处理等任务。但从应用程序员的角度来看,你从一个称为主线程的线程开始。这个线程有能力创建额外的线程,我们将在下一节中演示。

线程对象

原文:docs.oracle.com/javase/tutorial/essential/concurrency/threads.html

每个线程都与类Thread的实例相关联。使用Thread对象创建并发应用程序有两种基本策略。

  • 要直接控制线程的创建和管理,只需在应用程序需要启动异步任务时实例化Thread即可。

  • 要将线程管理与应用程序的其余部分抽象出来,将应用程序的任务传递给一个执行器

本节介绍了Thread对象的使用。执行器与其他高级并发对象一起讨论。

定义和启动线程

原文:docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html

创建 Thread 实例的应用程序必须提供将在该线程中运行的代码。 有两种方法可以做到这一点:

  • 提供一个 Runnable 对象. Runnable 接口定义了一个名为 run 的方法,用于包含在线程中执行的代码。 Runnable 对象被传递给 Thread 构造函数,就像 HelloRunnable 示例中那样:

    
    public class HelloRunnable implements Runnable {
    
        public void run() {
            System.out.println("Hello from a thread!");
        }
    
        public static void main(String args[]) {
            (new Thread(new HelloRunnable())).start();
        }
    
    }
    
    
  • 子类 Thread. Thread 类本身实现了 Runnable,尽管它的 run 方法什么也不做。 应用程序可以子类化 Thread,提供自己的 run 实现,就像 HelloThread 示例中那样:

    
    public class HelloThread extends Thread {
    
        public void run() {
            System.out.println("Hello from a thread!");
        }
    
        public static void main(String args[]) {
            (new HelloThread()).start();
        }
    
    }
    
    

请注意,这两个示例都调用了 Thread.start 来启动新线程。

你应该使用哪种习语?第一个习语使用了一个 Runnable 对象,更通用,因为 Runnable 对象可以是 Thread 以外的类的子类。 第二个习语在简单应用程序中更容易使用,但受到任务类必须是 Thread 的后代的限制。 本课程重点介绍第一种方法,它将 Runnable 任务与执行任务的 Thread 对象分开。 这种方法不仅更灵活,而且适用于后面介绍的高级线程管理 API。

Thread 类定义了一些对线程管理有用的方法。 这些方法包括 static 方法,提供有关调用方法的线程的信息或影响其状态。 其他方法是从参与管理线程和 Thread 对象的其他线程调用的。 我们将在以下部分中检查其中一些方法。

暂停执行与睡眠

原文:docs.oracle.com/javase/tutorial/essential/concurrency/sleep.html

Thread.sleep会导致当前线程暂停执行一段指定的时间。这是一种有效的方式,可以让处理器时间可用于应用程序的其他线程或者可能在计算机系统上运行的其他应用程序。sleep方法也可以用于节奏控制,就像下面的示例中展示的那样,以及等待另一个线程,该线程的任务被理解为具有时间要求,就像稍后章节中的SimpleThreads示例一样。

提供了两个重载版本的sleep:一个指定以毫秒为单位的睡眠时间,另一个指定以纳秒为单位的睡眠时间。然而,这些睡眠时间不能保证是精确的,因为它们受到底层操作系统提供的设施的限制。此外,睡眠时间可以被中断,我们将在稍后的章节中看到。无论如何,你不能假设调用sleep会精确地暂停线程指定的时间段。

SleepMessages示例使用sleep以四秒的间隔打印消息:


public class SleepMessages {
    public static void main(String args[])
        throws InterruptedException {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };

        for (int i = 0;
             i < importantInfo.length;
             i++) {
            //Pause for 4 seconds
            Thread.sleep(4000);
            //Print a message
            System.out.println(importantInfo[i]);
        }
    }
}

注意,main声明了它会throws InterruptedException。这是一个异常,当另一个线程在sleep处于活动状态时中断当前线程时会抛出。由于这个应用程序没有定义另一个线程来引起中断,所以它不会去捕获InterruptedException

中断

原文:docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html

中断是对线程的指示,告诉它应该停止当前操作并执行其他操作。程序员需要决定线程如何响应中断,但通常线程会终止。这是本课程强调的用法。

一个线程通过在要中断的线程的Thread对象上调用interrupt来发送中断。为了使中断机制正常工作,被中断的线程必须支持自身的中断。

支持中断

一个线程如何支持自身的中断?这取决于它当前正在做什么。如果线程频繁调用抛出InterruptedException的方法,它只需在捕获异常后从run方法返回。例如,假设SleepMessages示例中的中央消息循环在线程的Runnable对象的run方法中。那么可以修改如下以支持中断:

for (int i = 0; i < importantInfo.length; i++) {
    // Pause for 4 seconds
    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        // We've been interrupted: no more messages.
        return;
    }
    // Print a message
    System.out.println(importantInfo[i]);
}

许多抛出InterruptedException的方法,如sleep,设计为在接收到中断时取消当前操作并立即返回。

如果一个线程长时间不调用抛出InterruptedException的方法会怎样?那么它必须定期调用Thread.interrupted,如果接收到中断则返回true。例如:

for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        // We've been interrupted: no more crunching.
        return;
    }
}

在这个简单的例子中,代码只是检测中断并在接收到中断时退出线程。在更复杂的应用程序中,抛出InterruptedException可能更合理:

if (Thread.interrupted()) {
    throw new InterruptedException();
}

这使得中断处理代码可以集中在catch子句中。

中断状态标志

中断机制是通过一个称为中断状态的内部标志实现的。调用Thread.interrupt会设置这个标志。当线程通过调用静态方法Thread.interrupted检查中断时,中断状态会被清除。非静态的isInterrupted方法用于一个线程查询另一个线程的中断状态,不会改变中断状态标志。

按照惯例,任何通过抛出InterruptedException退出的方法在这样做时会清除中断状态。然而,另一个线程调用interrupt可能会立即再次设置中断状态。

加入

原文:docs.oracle.com/javase/tutorial/essential/concurrency/join.html

join方法允许一个线程等待另一个线程的完成。如果t是一个当前正在执行的线程的Thread对象,

t.join();

会导致当前线程暂停执行,直到t的线程终止。join的重载允许程序员指定等待时间。然而,与sleep一样,join依赖于操作系统的时间控制,因此你不应该假设join会等待与你指定的时间完全相同。

sleep一样,join在收到InterruptedException时会退出。

简单线程示例

原文:docs.oracle.com/javase/tutorial/essential/concurrency/simple.html

以下示例汇集了本节中的一些概念。SimpleThreads 包含两个线程。第一个是每个 Java 应用程序都有的主线程。主线程从 Runnable 对象 MessageLoop 创建一个新线程,并等待其完成。如果 MessageLoop 线程花费太长时间才能完成,主线程会中断它。

MessageLoop 线程会打印一系列消息。如果在打印完所有消息之前被中断,MessageLoop 线程会打印一条消息然后退出。


public class SimpleThreads {

    // Display a message, preceded by
    // the name of the current thread
    static void threadMessage(String message) {
        String threadName =
            Thread.currentThread().getName();
        System.out.format("%s: %s%n",
                          threadName,
                          message);
    }

    private static class MessageLoop
        implements Runnable {
        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            try {
                for (int i = 0;
                     i < importantInfo.length;
                     i++) {
                    // Pause for 4 seconds
                    Thread.sleep(4000);
                    // Print a message
                    threadMessage(importantInfo[i]);
                }
            } catch (InterruptedException e) {
                threadMessage("I wasn't done!");
            }
        }
    }

    public static void main(String args[])
        throws InterruptedException {

        // Delay, in milliseconds before
        // we interrupt MessageLoop
        // thread (default one hour).
        long patience = 1000 * 60 * 60;

        // If command line argument
        // present, gives patience
        // in seconds.
        if (args.length > 0) {
            try {
                patience = Long.parseLong(args[0]) * 1000;
            } catch (NumberFormatException e) {
                System.err.println("Argument must be an integer.");
                System.exit(1);
            }
        }

        threadMessage("Starting MessageLoop thread");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MessageLoop());
        t.start();

        threadMessage("Waiting for MessageLoop thread to finish");
        // loop until MessageLoop
        // thread exits
        while (t.isAlive()) {
            threadMessage("Still waiting...");
            // Wait maximum of 1 second
            // for MessageLoop thread
            // to finish.
            t.join(1000);
            if (((System.currentTimeMillis() - startTime) > patience)
                  && t.isAlive()) {
                threadMessage("Tired of waiting!");
                t.interrupt();
                // Shouldn't be long now
                // -- wait indefinitely
                t.join();
            }
        }
        threadMessage("Finally!");
    }
}

同步

原文:docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

线程主要通过共享对字段和对象引用字段引用的访问来进行通信。这种形式的通信非常高效,但可能导致两种错误:线程干扰内存一致性错误。防止这些错误所需的工具是同步

然而,同步可能引入线程争用,当两个或更多线程尝试同时访问同一资源导致 Java 运行时执行一个或多个线程更慢,甚至暂停它们的执行时发生。饥饿和活锁是线程争用的形式。有关更多信息,请参阅 Liveness 部分。

本节涵盖以下主题:

  • 线程干扰描述了当多个线程访问共享数据时引入错误的情况。

  • 内存一致性错误描述了由共享内存不一致视图引起的错误。

  • 同步方法描述了一种简单的习语,可以有效地防止线程干扰和内存一致性错误。

  • 隐式锁和同步描述了一种更通用的同步习语,并描述了同步是基于隐式锁的。

  • 原子访问讨论了无法被其他线程干扰的操作的一般概念。

线程干扰

原文:docs.oracle.com/javase/tutorial/essential/concurrency/interfere.html

考虑一个简单的名为 Counter 的类


class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

Counter 被设计成每次调用 increment 都会将 c 加 1,每次调用 decrement 都会从 c 减 1。然而,如果从多个线程引用 Counter 对象,线程之间的干扰可能会阻止预期的操作发生。

当两个操作在不同线程中运行,但作用于相同数据时,干扰就会发生。这意味着这两个操作由多个步骤组成,步骤序列会交叉。

对于 Counter 实例的操作似乎不可能交错,因为对 c 的操作都是单个简单语句。然而,即使是简单语句也可以被虚拟机翻译为多个步骤。我们不会检查虚拟机执行的具体步骤 — 知道单个表达式 c++ 可以分解为三个步骤就足够了:

  1. 检索当前值 c

  2. 递增检索到的值 1。

  3. 将递增后的值存储回 c

表达式 c-- 可以以相同方式分解,只是第二步是减少而不是增加。

假设线程 A 大约在同一时间调用 increment,而线程 B 调用 decrement。如果 c 的初始值为 0,它们交错的操作可能会按照这个顺序进行:

  1. 线程 A:检索 c。

  2. 线程 B:检索 c。

  3. 线程 A:递增检索到的值;结果为 1。

  4. 线程 B:减少检索到的值;结果为 -1。

  5. 线程 A:将结果存储在 c 中;c 现在为 1。

  6. 线程 B:将结果存储在 c 中;c 现在为 -1。

线程 A 的结果丢失,被线程 B 覆盖。这种特定的交错只是一种可能性。在不同情况下,可能会丢失线程 B 的结果,或者根本没有错误。由于它们是不可预测的,线程干扰 bug 可能很难检测和修复。

内存一致性错误

原文:docs.oracle.com/javase/tutorial/essential/concurrency/memconsist.html

内存一致性错误发生在不同线程对应该是相同数据的不一致视图时。内存一致性错误的原因复杂,超出了本教程的范围。幸运的是,程序员不需要详细了解这些原因。所需的只是避免它们的策略。

避免内存一致性错误的关键在于理解happens-before关系。这种关系简单地保证了一个特定语句的内存写入对另一个特定语句是可见的。为了看到这一点,考虑以下示例。假设定义并初始化了一个简单的int字段:

int counter = 0;

counter字段在两个线程 A 和 B 之间共享。假设线程 A 增加counter

counter++;

然后,不久之后,线程 B 打印出counter

System.out.println(counter);

如果这两个语句在同一个线程中执行,可以安全地假设打印出的值为"1"。但如果这两个语句在不同的线程中执行,打印出的值可能是"0",因为不能保证线程 A 对counter的更改对线程 B 可见,除非程序员在这两个语句之间建立了一个 happens-before 关系。

有几种动作会创建先于关系。其中之一是同步,我们将在接下来的部分中看到。

我们已经看到了两个创建先于关系的动作。

  • 当一个语句调用Thread.start时,与该语句具有先于关系的每个语句也与新线程执行的每个语句具有先于关系。导致创建新线程的代码的效果对新线程可见。

  • 当一个线程终止并导致另一个线程中的Thread.join返回时,那么终止线程执行的所有语句与成功加入后面的所有语句之间存在先于关系。线程中代码的效果现在对执行加入的线程可见。

有关创建先于关系的动作列表,请参考Java java.util.concurrent包的摘要页面。。

同步方法

原文:docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html

Java 编程语言提供了两种基本的同步习语:同步方法同步语句。其中更复杂的同步语句将在下一节中描述。本节讨论的是同步方法。

要使方法同步,只需在其声明中添加synchronized关键字:

public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

如果countSynchronizedCounter的一个实例,则使这些方法同步会产生两个效果:

  • 首先,不可能让同一对象上的两次同步方法调用交错。当一个线程正在为对象执行同步方法时,所有调用同一对象的同步方法的其他线程都会被阻塞(暂停执行),直到第一个线程完成对象的操作。

  • 其次,当一个同步方法退出时,它会自动与同一对象的任何后续同步方法的调用建立 happens-before 关系。这确保了对对象状态的更改对所有线程都是可见的。

请注意,构造函数不能被同步 — 使用synchronized关键字与构造函数是语法错误。同步构造函数没有意义,因为只有创建对象的线程在构造对象时应该访问它。


警告: 在构建一个将在多个线程之间共享的对象时,一定要非常小心,确保对象的引用不会过早“泄漏”。例如,假设你想要维护一个名为instancesList,其中包含类的每个实例。你可能会诱惑地在构造函数中添加以下行:

instances.add(this);

但是其他线程可以使用instances来访问对象,而在对象构造完成之前。


同步方法为防止线程干扰和内存一致性错误提供了一种简单的策略:如果一个对象对多个线程可见,那么对该对象的变量的所有读取或写入都通过synchronized方法进行。 (一个重要的例外:final字段,在对象构造后无法修改,可以通过非同步方法安全地读取,一旦对象构造完成)这种策略是有效的,但在后面的课程中我们将看到它可能会出现 liveness 问题。

内在锁和同步

原文:docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

同步建立在一个称为内在锁监视器锁的内部实体周围。内在锁在同步的两个方面发挥作用:强制对对象状态的独占访问和建立对可见性至关重要的 happens-before 关系。

每个对象都有一个与之关联的内在锁。按照惯例,需要独占和一致访问对象字段的线程在访问这些字段之前必须获取对象的内在锁,然后在完成后释放内在锁。线程在获取锁和释放锁之间被认为拥有内在锁。只要一个线程拥有内在锁,其他线程就无法获取相同的锁。当另一个线程尝试获取锁时,它将被阻塞。

当一个线程释放一个内在锁时,该操作与后续获取相同锁的任何操作建立 happens-before 关系。

同步方法中的锁

当一个线程调用一个同步方法时,它会自动获取该方法对象的内在锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,锁也会被释放。

你可能会想知道当调用静态同步方法时会发生什么,因为静态方法与类相关联,而不是对象。在这种情况下,线程会获取与类相关联的Class对象的内在锁。因此,对类的静态字段的访问受到一个与类的任何实例的锁不同的锁的控制。

同步语句

创建同步代码的另一种方法是使用synchronized 语句。与同步方法不同,同步语句必须指定提供内在锁的对象:

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

在这个例子中,addName方法需要同步对lastNamenameCount的更改,但也需要避免同步调用其他对象的方法。(从同步代码调用其他对象的方法可能会导致在 Liveness 部分描述的问题。)如果没有同步语句,就必须有一个单独的、非同步的方法,唯一目的是调用nameList.add

同步语句也有助于通过细粒度同步提高并发性。例如,假设类MsLunch有两个实例字段,c1c2,它们永远不会同时使用。所有这些字段的更新必须同步,但没有理由阻止对 c1 的更新与对 c2 的更新交错—这样做会通过创建两个仅用于提供锁的对象来减少并发性。

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

使用这种习语要非常小心。你必须绝对确定交错访问受影响字段是安全的。

可重入同步

请记住,一个线程不能获取另一个线程拥有的锁。但一个线程可以获取它已经拥有的锁。允许一个线程多次获取相同的锁使可重入同步成为可能。这描述了一种情况,即同步代码直接或间接地调用一个也包含同步代码的方法,并且两组代码使用相同的锁。如果没有可重入同步,同步代码将不得不采取许多额外的预防措施,以避免一个线程导致自己被阻塞。

原子访问

原文:docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

在编程中,原子操作是一种有效地一次性完成的操作。原子操作不能在中途停止:它要么完全发生,要么根本不发生。在原子操作完成之前,不会看到任何副作用。

我们已经看到增量表达式,比如c++,并不描述原子操作。即使是非常简单的表达式也可以定义可以分解为其他操作的复杂操作。然而,有一些操作是可以指定为原子操作的:

  • 对于引用变量和大多数基本变量(除了longdouble之外的所有类型),读取和写入是原子的。

  • 对于所有声明为volatile的变量,读取和写入都是原子的(包括longdouble变量)。

原子操作不能交错,因此可以在不担心线程干扰的情况下使用它们。然而,这并不消除同步原子操作的所有需求,因为内存一致性错误仍然可能发生。使用volatile变量可以减少内存一致性错误的风险,因为对volatile变量的任何写入都会与随后对该变量的读取建立 happens-before 关系。这意味着对volatile变量的更改始终对其他线程可见。更重要的是,这也意味着当线程读取volatile变量时,它不仅看到volatile的最新更改,还看到导致更改的代码的副作用。

使用简单的原子变量访问比通过同步代码访问这些变量更有效,但需要程序员更加小心,以避免内存一致性错误。额外的努力是否值得取决于应用程序的大小和复杂性。

java.util.concurrent包中的一些类提供了不依赖于同步的原子方法。我们将在高级并发对象部分讨论它们。

活跃性

原文:docs.oracle.com/javase/tutorial/essential/concurrency/liveness.html

并发应用程序按时执行的能力被称为其liveness。本节描述了最常见的活跃性问题,死锁,并简要描述了另外两种活跃性问题,饥饿和活锁。

死锁

原文:docs.oracle.com/javase/tutorial/essential/concurrency/deadlock.html

死锁 描述了两个或更多线程永远被阻塞,彼此等待的情况。这里有一个例子。

阿方索和加斯顿是朋友,也是极信奉礼貌的人。一个严格的礼貌规则是,当你向朋友鞠躬时,你必须保持鞠躬的姿势,直到你的朋友有机会回礼。不幸的是,这个规则没有考虑到两个朋友可能同时向对方鞠躬的可能性。这个示例应用程序,死锁,模拟了这种可能性:


public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

死锁 运行时,当它们尝试调用 bowBack 时,两个线程都很可能被阻塞。由于每个线程都在等待另一个线程退出 bow,因此这两个阻塞永远不会结束。

饥饿和活锁

原文:docs.oracle.com/javase/tutorial/essential/concurrency/starvelive.html

饥饿和活锁问题比死锁问题要少见得多,但仍然是每个并发软件设计者可能会遇到的问题。

饥饿

饥饿描述了一个线程无法定期访问共享资源并且无法取得进展的情况。这种情况发生在"贪婪"线程长时间地使共享资源不可用时。例如,假设一个对象提供了一个经常需要很长时间才能返回的同步方法。如果一个线程频繁调用这个方法,其他也需要频繁同步访问同一对象的线程将经常被阻塞。

活锁

一个线程经常是作为对另一个线程动作的响应。如果另一个线程的动作也是对另一个线程动作的响应,那么可能会发生livelock。与死锁类似,活锁的线程无法取得进一步的进展。然而,这些线程并没有被阻塞 — 它们只是忙于相互响应而无法恢复工作。这就好比两个人试图在走廊里互相让对方通过:阿方斯向左移动让加斯通通过,而加斯通向右移动让阿方斯通过。看到他们仍然互相阻挡,阿方斯向右移动,而加斯通向左移动。他们仍然互相阻挡,所以...

保护块

原文:docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

线程经常需要协调它们的动作。最常见的协调习语是保护块。这样的块开始于轮询一个条件,该条件必须在块可以继续之前为真。为了正确执行此操作,需要遵循一些步骤。

假设,例如guardedJoy是一个方法,必须在另一个线程设置共享变量joy之前才能继续。这样的方法理论上可以简单地循环,直到条件满足,但是该循环是浪费的,因为它在等待时持续执行。

public void guardedJoy() {
    // Simple loop guard. Wastes
    // processor time. Don't do this!
    while(!joy) {}
    System.out.println("Joy has been achieved!");
}

一个更有效的保护块调用Object.wait来挂起当前线程。调用wait不会返回,直到另一个线程发出通知,表明可能发生了某个特殊事件,尽管不一定是该线程正在等待的事件:

public synchronized void guardedJoy() {
    // This guard only loops once for each special event, which may not
    // be the event we're waiting for.
    while(!joy) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Joy and efficiency have been achieved!");
}


注意:始终在测试等待的条件的循环中调用wait。不要假设中断是为了您正在等待的特定条件,或者该条件仍然为真。


像许多暂停执行的方法一样,wait可能会抛出InterruptedException。在这个例子中,我们可以忽略这个异常,我们只关心joy的值。

为什么这个guardedJoy的版本是同步的?假设d是我们用来调用wait的对象。当一个线程调用d.wait时,它必须拥有d的内在锁,否则会抛出错误。在同步方法中调用wait是获取内在锁的简单方法。

当调用wait时,线程释放锁并暂停执行。在将来的某个时间,另一个线程将获得相同的锁并调用Object.notifyAll,通知所有等待该锁的线程发生了重要事件:

public synchronized notifyJoy() {
    joy = true;
    notifyAll();
}

第二个线程释放锁后一段时间,第一个线程重新获取锁,并通过从wait调用返回来恢复执行。


注意:还有第二种通知方法,notify,它唤醒单个线程。因为notify不允许您指定被唤醒的线程,所以它只在大规模并行应用程序中有用,即具有大量线程的程序,所有线程都在做类似的工作。在这种应用程序中,您不关心哪个线程被唤醒。


让我们使用保护块来创建一个生产者-消费者应用程序。这种应用程序在两个线程之间共享数据:生产者创建数据,消费者对其进行处理。这两个线程使用共享对象进行通信。协调是必不可少的:消费者线程在生产者线程交付数据之前不得尝试检索数据,生产者线程在消费者尚未检索旧数据之前不得尝试交付新数据。

在这个例子中,数据是一系列文本消息,通过一个类型为Drop的对象共享:


public class Drop {
    // Message sent from producer
    // to consumer.
    private String message;
    // True if consumer should wait
    // for producer to send message,
    // false if producer should wait for
    // consumer to retrieve message.
    private boolean empty = true;

    public synchronized String take() {
        // Wait until message is
        // available.
        while (empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = true;
        // Notify producer that
        // status has changed.
        notifyAll();
        return message;
    }

    public synchronized void put(String message) {
        // Wait until message has
        // been retrieved.
        while (!empty) {
            try { 
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = false;
        // Store message.
        this.message = message;
        // Notify consumer that status
        // has changed.
        notifyAll();
    }
}

生产者线程,在Producer中定义,发送一系列熟悉的消息。字符串"DONE"表示所有消息都已发送。为了模拟真实应用程序的不可预测性,生产者线程在消息之间暂停一段随机时间。


import java.util.Random;

public class Producer implements Runnable {
    private Drop drop;

    public Producer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
        Random random = new Random();

        for (int i = 0;
             i < importantInfo.length;
             i++) {
            drop.put(importantInfo[i]);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
        drop.put("DONE");
    }
}

消费者线程,在Consumer中定义,简单地检索消息并打印出来,直到检索到"DONE"字符串为止。该线程还会暂停一段随机时间。


import java.util.Random;

public class Consumer implements Runnable {
    private Drop drop;

    public Consumer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        Random random = new Random();
        for (String message = drop.take();
             ! message.equals("DONE");
             message = drop.take()) {
            System.out.format("MESSAGE RECEIVED: %s%n", message);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
    }
}

最后,这是主线程,在ProducerConsumerExample中定义,启动生产者和消费者线程。


public class ProducerConsumerExample {
    public static void main(String[] args) {
        Drop drop = new Drop();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}


注意: Drop 类是为了演示受保护的代码块而编写的。在尝试编写自己的数据共享对象之前,请查看 Java 集合框架中的现有数据结构,以避免重复造轮子。有关更多信息,请参考问题和练习部分。


不可变对象

原文:docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html

如果一个对象在构造后其状态不能改变,则被认为是不可变的。广泛接受的一种创建简单可靠代码的策略是最大程度地依赖不可变对象。

不可变对象在并发应用程序中特别有用。由于它们不能改变状态,因此它们不会受到线程干扰的破坏,也不会以不一致的状态被观察到。

程序员通常不愿使用不可变对象,因为他们担心创建一个新对象的成本,而不是就地更新对象。对象创建的影响经常被高估,可以通过一些与不可变对象相关的效率来抵消。这些效率包括由于垃圾回收而减少的开销,以及消除了为了保护可变对象免受破坏而需要的代码。

以下小节以一个实例是可变的类为例,并从中派生出一个实例是不可变的类。这样做,它们给出了这种转换的一般规则,并展示了不可变对象的一些优势。

一个同步类的示例

原文:docs.oracle.com/javase/tutorial/essential/concurrency/syncrgb.html

这个类,SynchronizedRGB,定义了代表颜色的对象。每个对象将颜色表示为三个代表主要颜色值的整数和一个给出颜色名称的字符串。


public class SynchronizedRGB {

    // Values must be between 0 and 255.
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red,
                           int green,
                           int blue,
                           String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red,
                    int green,
                    int blue,
                    String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }

    public synchronized void invert() {
        red = 255 - red;
        green = 255 - green;
        blue = 255 - blue;
        name = "Inverse of " + name;
    }
}

必须小心使用SynchronizedRGB,以避免出现不一致的状态。例如,假设一个线程执行以下代码:

SynchronizedRGB color =
    new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB();      //Statement 1
String myColorName = color.getName(); //Statement 2

如果另一个线程在语句 1 之后但在语句 2 之前调用color.setmyColorInt的值将不匹配myColorName的值。为了避免这种结果,这两个语句必须绑定在一起:

synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
} 

这种不一致性只对可变对象有效 — 对于不可变版本的SynchronizedRGB不会有问题。

定义不可变对象的策略

原文:docs.oracle.com/javase/tutorial/essential/concurrency/imstrat.html

以下规则定义了创建不可变对象的简单策略。并非所有被记录为“不可变”的类都遵循这些规则。这并不一定意味着这些类的创建者粗心大意 — 他们可能有充分的理由相信他们的类的实例在构造后永远不会改变。然而,这种策略需要复杂的分析,不适合初学者。

  1. 不提供“setter”方法 — 修改字段或字段引用的对象的方法。

  2. 使所有字段都是finalprivate

  3. 不允许子类重写方法。这样做的最简单方法是将类声明为final。更复杂的方法是将构造函数设为private,并在工厂方法中构造实例。

  4. 如果实例字段包括对可变对象的引用,请不要允许更改这些对象:

    • 不要提供修改可变对象的方法。

    • 不共享对可变对象的引用。永远不要存储传递给构造函数的外部可变对象的引用;如果必要,创建副本,并存储对副本的引用。类似地,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

将这种策略应用于SynchronizedRGB会产生以下步骤:

  1. 这个类中有两个 setter 方法。第一个set方法任意地转换对象,并且在类的不可变版本中没有位置。第二个invert方法可以通过创建一个新对象来适应,而不是修改现有对象。

  2. 所有字段已经是private;它们进一步被标记为final

  3. 类本身被声明为final

  4. 只有一个字段引用一个对象,而该对象本身是不可变的。因此,不需要防止改变“包含”可变对象状态的保护措施。

在这些更改之后,我们有了ImmutableRGB


final public class ImmutableRGB {

    // Values must be between 0 and 255.
    final private int red;
    final private int green;
    final private int blue;
    final private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red,
                        int green,
                        int blue,
                        String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }

    public ImmutableRGB invert() {
        return new ImmutableRGB(255 - red,
                       255 - green,
                       255 - blue,
                       "Inverse of " + name);
    }
}

高级并发对象

原文:docs.oracle.com/javase/tutorial/essential/concurrency/highlevel.html

到目前为止,本课程已经专注于 Java 平台从一开始就存在的低级 API。这些 API 对于非常基本的任务是足够的,但对于更高级的任务需要更高级的构建块。这对于充分利用当今的多处理器和多核系统的大规模并发应用程序尤为重要。

在本节中,我们将介绍 Java 平台 5.0 版本引入的一些高级并发特性。这些特性大多数都是在新的java.util.concurrent包中实现的。Java 集合框架中还有新的并发数据结构。

  • 锁对象支持简化许多并发应用程序的锁定习语。

  • 执行器定义了一个用于启动和管理线程的高级 API。java.util.concurrent提供的执行器实现提供了适用于大规模应用程序的线程池管理。

  • 并发集合使得管理大量数据集变得更加容易,并且可以大大减少同步的需求。

  • 原子变量具有最小化同步和避免内存一致性错误的特性。

  • ThreadLocalRandom(在 JDK 7 中)提供了多线程有效生成伪随机数的功能。

锁对象

原文:docs.oracle.com/javase/tutorial/essential/concurrency/newlocks.html

同步代码依赖于一种简单的可重入锁。这种类型的锁易于使用,但有许多限制。更复杂的锁习语由 java.util.concurrent.locks 包支持。我们不会详细讨论此包,而是专注于其最基本的接口 Lock

Lock 对象的工作方式与同步代码中使用的隐式锁非常相似。与隐式锁一样,一次只有一个线程可以拥有 Lock 对象。Lock 对象还支持通过其关联的 Condition 对象实现 wait/notify 机制。

Lock 对象相对于隐式锁的最大优势在于其能够在尝试获取锁时撤销操作。如果指定了超时时间,tryLock 方法在锁不可用时或超时之前会撤销操作。lockInterruptibly 方法在获取锁之前如果另一个线程发送中断信号,则会撤销操作。

让我们使用 Lock 对象来解决我们在 Liveness 中看到的死锁问题。阿方索和加斯顿已经训练自己注意到朋友即将鞠躬的时刻。我们通过要求我们的 Friend 对象必须在继续鞠躬之前为两个参与者获取锁来模拟这种改进。这是改进模型的源代码,Safelock。为了展示这种习语的多功能性,我们假设阿方索和加斯顿如此迷恋他们新发现的安全鞠躬能力,以至于他们无法停止向彼此鞠躬:


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;

public class Safelock {
    static class Friend {
        private final String name;
        private final Lock lock = new ReentrantLock();

        public Friend(String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }

        public boolean impendingBow(Friend bower) {
            Boolean myLock = false;
            Boolean yourLock = false;
            try {
                myLock = lock.tryLock();
                yourLock = bower.lock.tryLock();
            } finally {
                if (! (myLock && yourLock)) {
                    if (myLock) {
                        lock.unlock();
                    }
                    if (yourLock) {
                        bower.lock.unlock();
                    }
                }
            }
            return myLock && yourLock;
        }

        public void bow(Friend bower) {
            if (impendingBow(bower)) {
                try {
                    System.out.format("%s: %s has"
                        + " bowed to me!%n", 
                        this.name, bower.getName());
                    bower.bowBack(this);
                } finally {
                    lock.unlock();
                    bower.lock.unlock();
                }
            } else {
                System.out.format("%s: %s started"
                    + " to bow to me, but saw that"
                    + " I was already bowing to"
                    + " him.%n",
                    this.name, bower.getName());
            }
        }

        public void bowBack(Friend bower) {
            System.out.format("%s: %s has" +
                " bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    static class BowLoop implements Runnable {
        private Friend bower;
        private Friend bowee;

        public BowLoop(Friend bower, Friend bowee) {
            this.bower = bower;
            this.bowee = bowee;
        }

        public void run() {
            Random random = new Random();
            for (;;) {
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {}
                bowee.bow(bower);
            }
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new BowLoop(alphonse, gaston)).start();
        new Thread(new BowLoop(gaston, alphonse)).start();
    }
}

Executors

原文:docs.oracle.com/javase/tutorial/essential/concurrency/executors.html

在所有先前的示例中,新线程执行的任务与其Runnable对象定义的线程本身(由Thread对象定义)之间存在密切联系。这对于小型应用程序效果很好,但在大型应用程序中,将线程管理和创建与应用程序的其余部分分离是有意义的。封装这些功能的对象称为executors。以下小节详细描述了 executors。

  • Executor Interfaces 定义了三种 executor 对象类型。

  • Thread Pools 是最常见的 executor 实现类型。

  • Fork/Join 是一个利用多处理器的框架(JDK 7 中新增)。

执行器接口

原文:docs.oracle.com/javase/tutorial/essential/concurrency/exinter.html

java.util.concurrent 包定义了三个执行器接口:

  • Executor 是一个简单的接口,支持启动新任务。

  • ExecutorServiceExecutor 的子接口,增加了一些功能,有助于管理单个任务和执行器本身的生命周期。

  • ScheduledExecutorServiceExecutorService 的子接口,支持未来和/或定期执行任务。

通常,引用执行器对象的变量声明为这三种接口类型之一,而不是执行器类类型。

Executor 接口

Executor 接口提供了一个方法 execute,旨在成为常见线程创建习语的替代品。如果 r 是一个 Runnable 对象,e 是一个 Executor 对象,你可以替换

(new Thread(r)).start();

with

e.execute(r);

然而,execute 的定义不太具体。低级习语创建一个新线程并立即启动它。根据 Executor 的实现,execute 可能会做同样的事情,但更有可能使用现有的工作线程来运行 r,或者将 r 放入队列等待工作线程可用。(我们将在线程池部分描述工作线程。)

java.util.concurrent 中的执行器实现旨在充分利用更高级的 ExecutorServiceScheduledExecutorService 接口,尽管它们也与基本的 Executor 接口一起工作。

ExecutorService 接口

ExecutorService 接口通过类似但更灵活的 submit 方法来补充 execute。与 execute 一样,submit 接受 Runnable 对象,但也接受 Callable 对象,允许任务返回一个值。submit 方法返回一个 Future 对象,用于检索 Callable 返回值并管理 CallableRunnable 任务的状态。

ExecutorService 还提供了提交大量 Callable 对象的方法。最后,ExecutorService 提供了一些方法来管理执行器的关闭。为了支持立即关闭,任务应正确处理中断。

ScheduledExecutorService 接口

ScheduledExecutorService 接口通过 schedule 补充了其父接口 ExecutorService 的方法,该方法在指定延迟后执行 RunnableCallable 任务。此外,该接口定义了 scheduleAtFixedRatescheduleWithFixedDelay,以在定义的间隔时间内重复执行指定任务。

线程池

原文:docs.oracle.com/javase/tutorial/essential/concurrency/pools.html

java.util.concurrent 中的大多数执行器实现使用线程池,其中包含工作线程。这种类型的线程与它执行的RunnableCallable任务分开存在,并经常用于执行多个任务。

使用工作线程可以最小化由于线程创建而产生的开销。线程对象使用大量内存,在大规模应用程序中,分配和释放许多线程对象会产生显著的内存管理开销。

一种常见的线程池类型是固定线程池。这种类型的池始终有指定数量的线程在运行;如果某个线程在仍在使用时被终止,它将自动被新线程替换。任务通过内部队列提交到池中,当活动任务多于线程时,队列会保存额外的任务。

使用固定线程池的一个重要优势是应用程序在使用它时优雅降级。要理解这一点,考虑一个 Web 服务器应用程序,其中每个 HTTP 请求都由一个单独的线程处理。如果应用程序只是为每个新的 HTTP 请求创建一个新线程,并且系统接收到的请求多于它立即处理的能力,当所有这些线程的开销超过系统容量时,应用程序将突然停止响应所有请求。通过限制可以创建的线程数量,应用程序将不会像请求进来那样快速地为 HTTP 请求提供服务,但它将以系统能够维持的速度为它们提供服务。

创建使用固定线程池的执行器的简单方法是在java.util.concurrent.Executors中调用newFixedThreadPool工厂方法。该类还提供以下工厂方法:

  • newCachedThreadPool方法创建一个具有可扩展线程池的执行器。此执行器适用于启动许多短暂任务的应用程序。

  • newSingleThreadExecutor方法创建一个一次执行一个任务的执行器。

  • 几个工厂方法是上述执行器的ScheduledExecutorService版本。

如果上述工厂方法提供的任何执行器都不符合您的需求,构造java.util.concurrent.ThreadPoolExecutorjava.util.concurrent.ScheduledThreadPoolExecutor的实例将为您提供额外的选项。

分叉/合并

原文:docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html

分叉/合并框架是ExecutorService接口的一种实现,可以帮助你充分利用多个处理器。它专为可以递归地分解为较小片段的工作而设计。目标是利用所有可用的处理能力来提高应用程序的性能。

与任何ExecutorService实现一样,分叉/合并框架将任务分配给线程池中的工作线程。分叉/合并框架的独特之处在于它使用工作窃取算法。工作线程如果没有任务可执行,可以从其他仍在忙碌的线程中窃取任务。

分叉/合并框架的核心是ForkJoinPool类,它是AbstractExecutorService类的扩展。ForkJoinPool实现了核心的工作窃取算法,并可以执行ForkJoinTask进程。

基本用法

使用分叉/合并框架的第一步是编写执行一部分工作的代码。你的代码应该类似于以下伪代码:

if (my portion of the work is small enough)
  do the work directly
else
  split my work into two pieces
  invoke the two pieces and wait for the results

将这段代码封装在一个ForkJoinTask子类中,通常使用其中的一个更专门的类型,要么是RecursiveTask(可以返回结果),要么是RecursiveAction

当你的ForkJoinTask子类准备就绪后,创建代表所有要完成工作的对象,并将其传递给ForkJoinPool实例的invoke()方法。

清晰的模糊

为了帮助你理解分叉/合并框架的工作原理,请考虑以下示例。假设你想要对图像进行模糊处理。原始图像由一个整数数组表示,其中每个整数包含单个像素的颜色值。模糊后的目标图像也由一个与源图像大小相同的整数数组表示。

执行模糊操作是通过逐个像素地处理源数组来完成的。每个像素与其周围像素(红色、绿色和蓝色分量取平均值)进行平均,结果放入目标数组中。由于图像是一个大数组,这个过程可能需要很长时间。你可以利用多处理器系统上的并发处理,使用分叉/合并框架来实现算法。以下是一个可能的实现:

public class ForkBlur extends RecursiveAction {
    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;

    // Processing window size; should be odd.
    private int mBlurWidth = 15;

    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    protected void computeDirectly() {
        int sidePixels = (mBlurWidth - 1) / 2;
        for (int index = mStart; index < mStart + mLength; index++) {
            // Calculate average.
            float rt = 0, gt = 0, bt = 0;
            for (int mi = -sidePixels; mi <= sidePixels; mi++) {
                int mindex = Math.min(Math.max(mi + index, 0),
                                    mSource.length - 1);
                int pixel = mSource[mindex];
                rt += (float)((pixel & 0x00ff0000) >> 16)
                      / mBlurWidth;
                gt += (float)((pixel & 0x0000ff00) >>  8)
                      / mBlurWidth;
                bt += (float)((pixel & 0x000000ff) >>  0)
                      / mBlurWidth;
            }

            // Reassemble destination pixel.
            int dpixel = (0xff000000     ) |
                   (((int)rt) << 16) |
                   (((int)gt) <<  8) |
                   (((int)bt) <<  0);
            mDestination[index] = dpixel;
        }
    }

  ...

现在你要实现抽象的compute()方法,该方法可以直接执行模糊操作,也可以将其拆分为两个较小的任务。一个简单的数组长度阈值有助于确定是执行工作还是拆分任务。

protected static int sThreshold = 100000;

protected void compute() {
    if (mLength < sThreshold) {
        computeDirectly();
        return;
    }

    int split = mLength / 2;

    invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
              new ForkBlur(mSource, mStart + split, mLength - split,
                           mDestination));
}

如果前面的方法在RecursiveAction类的子类中,那么设置任务在ForkJoinPool中运行就很简单,包括以下步骤:

  1. 创建一个代表所有要完成工作的任务。

    // source image pixels are in src
    // destination image pixels are in dst
    ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
    
    
  2. 创建将运行任务的ForkJoinPool

    ForkJoinPool pool = new ForkJoinPool();
    
    
  3. 运行任务。

    pool.invoke(fb);
    
    

要查看完整的源代码,包括一些额外的代码来创建目标图像文件,请参见ForkBlur示例。

标准实现

除了在多处理器系统上并行执行任务的自定义算法(例如前一节中的ForkBlur.java示例)中使用分支/合并框架之外,Java SE 中还有一些通用功能已经使用分支/合并框架实现。其中一种实现是在 Java SE 8 中引入的,被java.util.Arrays类用于其parallelSort()方法。这些方法类似于sort(),但通过分支/合并框架利用并发性能。在多处理器系统上运行时,大型数组的并行排序比顺序排序更快。然而,这些方法如何利用分支/合并框架超出了 Java 教程的范围。有关此信息,请参阅 Java API 文档。

另一个实现分支/合并框架的方法是使用java.util.streams包中的方法,该包是Project Lambda的一部分,计划在 Java SE 8 发布中使用。更多信息,请参阅 Lambda 表达式部分。

并发集合

原文:docs.oracle.com/javase/tutorial/essential/concurrency/collections.html

java.util.concurrent 包包含了许多对 Java 集合框架的补充。这些最容易通过提供的集合接口进行分类:

  • BlockingQueue 定义了一个先进先出的数据结构,当尝试向满队列添加或从空队列检索时会阻塞或超时。

  • ConcurrentMapjava.util.Map 的子接口,定义了有用的原子操作。这些操作仅在键存在时移除或替换键值对,或仅在键不存在时添加键值对。使这些操作原子化有助于避免同步。ConcurrentMap 的标准通用实现是 ConcurrentHashMap,它是 HashMap 的并发模拟。

  • ConcurrentNavigableMapConcurrentMap 的子接口,支持近似匹配。ConcurrentNavigableMap 的标准通用实现是 ConcurrentSkipListMap,它是 TreeMap 的并发模拟。

所有这些集合都有助于避免内存一致性错误,通过定义将一个对象添加到集合的操作与随后访问或移除该对象的操作之间的 happens-before 关系。

原子变量

原文:docs.oracle.com/javase/tutorial/essential/concurrency/atomicvars.html

java.util.concurrent.atomic包定义了支持单个变量上原子操作的类。所有类都有类似于对volatile变量进行读取和写入的getset方法。也就是说,set与同一变量上的任何后续get之间存在 happens-before 关系。原子compareAndSet方法也具有这些内存一致性特性,整数原子变量适用的简单原子算术方法也是如此。

要了解这个包可能如何使用,让我们回到最初用来演示线程干扰的Counter类:


class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

使Counter免受线程干扰的一种方法是使其方法同步,就像SynchronizedCounter中那样:


class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }

}

对于这个简单的类,同步是一个可接受的解决方案。但对于一个更复杂的类,我们可能希望避免不必要同步的活跃度影响。用AtomicInteger替换int字段可以让我们在不使用同步的情况下防止线程干扰,就像AtomicCounter中那样:


import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

并发随机数

原文:docs.oracle.com/javase/tutorial/essential/concurrency/threadlocalrandom.html

在 JDK 7 中,java.util.concurrent包含一个方便的类,ThreadLocalRandom,适用于期望从多个线程或ForkJoinTask中使用随机数的应用程序。

对于并发访问,使用ThreadLocalRandom而不是Math.random()会减少争用,最终提高性能。

你只需调用ThreadLocalRandom.current(),然后调用其中的方法来获取一个随机数。以下是一个示例:

int r = ThreadLocalRandom.current() .nextInt(4, 77);

进一步阅读

原文:docs.oracle.com/javase/tutorial/essential/concurrency/further.html

  • Concurrent Programming in Java: Design Principles and Pattern (2nd Edition) 作者:Doug Lea。这是一部由领先专家撰写的全面作品,他也是 Java 平台并发框架的架构师。

  • Java Concurrency in Practice 作者:Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, 和 Doug Lea。一本旨在让初学者易于理解的实用指南。

  • Effective Java Programming Language Guide (2nd Edition) 作者:Joshua Bloch。虽然这是一本通用的编程指南,但其中关于线程的章节包含了并发编程的基本“最佳实践”。

  • Concurrency: State Models & Java Programs (2nd Edition) 作者:Jeff Magee 和 Jeff Kramer。通过建模和实际示例相结合,介绍并发编程。

  • Java Concurrent Animated: 展示并发特性使用的动画。

问题和练习:并发

原文:docs.oracle.com/javase/tutorial/essential/concurrency/QandE/questions.html

问题

  1. 你能将Thread对象传递给Executor.execute吗?这样的调用有意义吗?

练习

  1. 编译并运行BadThreads.java

    
    public class BadThreads {
    
        static String message;
    
        private static class CorrectorThread
            extends Thread {
    
            public void run() {
                try {
                    sleep(1000); 
                } catch (InterruptedException e) {}
                // Key statement 1:
                message = "Mares do eat oats."; 
            }
        }
    
        public static void main(String args[])
            throws InterruptedException {
    
            (new CorrectorThread()).start();
            message = "Mares do not eat oats.";
            Thread.sleep(2000);
            // Key statement 2:
            System.out.println(message);
        }
    }
    
    

    应用程序应该打印出“Mares do eat oats.” 这是一定会发生的吗?如果不是,为什么?改变两次Sleep调用的参数会有帮助吗?如何确保所有对message的更改在主线程中可见?

  2. 修改 Guarded Blocks 中的生产者-消费者示例,使用标准库类代替Drop类。

检查你的答案。

课程:平台环境

原文:docs.oracle.com/javase/tutorial/essential/environment/index.html

应用程序在平台环境中运行,由底层操作系统、Java 虚拟机、类库和应用程序启动时提供的各种配置数据定义。本课程描述了应用程序用于检查和配置其平台环境的一些 API。本课程包括三个部分:

  • 配置工具描述了用于访问应用程序部署时提供的配置数据或应用程序用户提供的 API。

  • 系统工具描述了在SystemRuntime类中定义的各种 API。

  • 路径和类路径描述了用于配置 JDK 开发工具和其他应用程序的环境变量。

配置实用程序

原文:docs.oracle.com/javase/tutorial/essential/environment/config.html

这一部分描述了一些配置实用程序,帮助应用程序访问其启动上下文。

属性

原文:docs.oracle.com/javase/tutorial/essential/environment/properties.html

属性是作为键/值对管理的配置值。在每对中,键和值都是String值。键标识并用于检索值,就像使用变量名检索变量的值一样。例如,一个能够下载文件的应用程序可能使用名为“download.lastDirectory”的属性来跟踪用于最后下载的目录。

要管理属性,请创建java.util.Properties的实例。此类提供以下方法:

  • 从流中加载键/值对到Properties对象中,

  • 通过其键检索值,

  • 列出键及其值,

  • 枚举键,

  • 将属性保存到流中。

有关流的介绍,请参阅输入/输出流中的基本 I/O 课程。

Properties扩展了java.util.Hashtable。从Hashtable继承的一些方法支持以下操作:

  • 测试特定键或值是否在Properties对象中,

  • 获取当前键/值对的数量,

  • 删除键及其值,

  • Properties列表添加键/值对,

  • 枚举值或键,

  • 通过键检索值,

  • 查看Properties对象是否为空。


安全注意事项: 访问属性需经当前安全管理器批准。本节中的示例代码段假定为独立应用程序,这些应用程序默认没有安全管理器。同样的代码在 applet 中可能无法正常工作,具体取决于运行的浏览器。请参阅 Applet 的功能和限制中的 Java Applets 课程,了解 applet 的安全限制信息。


System类维护一个定义当前工作环境配置的Properties对象。有关这些属性的更多信息,请参阅系统属性。本节的其余部分将解释如何使用属性来管理应用程序配置。

应用程序生命周期中的属性

以下图示说明了典型应用程序如何在执行过程中使用Properties对象管理其配置数据。

Properties 对象的可能生命周期

  • 启动中

    第一个三个框中给出的操作发生在应用程序启动时。首先,应用程序将默认属性从一个众所周知的位置加载到Properties对象中。通常,默认属性存储在磁盘上的文件中,与应用程序的.class和其他资源文件一起。

    接下来,应用程序创建另一个Properties对象,并加载上次运行应用程序时保存的属性。许多应用程序按用户为单位存储属性,因此此步骤中加载的属性通常位于应用程序在用户主目录中维护的特定目录中的特定文件中。最后,应用程序使用默认和记忆的属性来初始化自身。

    关键在于一致性。应用程序必须始终将属性加载和保存到相同位置,以便下次执行时能够找到它们。

  • 运行中

    在应用程序执行期间,用户可能会更改一些设置,也许在首选项窗口中,并且Properties对象将更新以反映这些更改。如果要记住用户更改以供将来的会话使用,则必须保存这些更改。

  • 退出

    退出时,应用程序将属性保存到其众所周知的位置,以便在下次启动应用程序时再次加载。

设置Properties对象

以下 Java 代码执行了前一节描述的前两个步骤:加载默认属性和加载记住的属性:

. . .
// create and load default properties
Properties defaultProps = new Properties();
FileInputStream in = new FileInputStream("defaultProperties");
defaultProps.load(in);
in.close();

// create application properties with default
Properties applicationProps = new Properties(defaultProps);

// now load properties 
// from last invocation
in = new FileInputStream("appProperties");
applicationProps.load(in);
in.close();
. . .

首先,应用程序设置一个默认的Properties对象。该对象包含一组属性,如果在其他地方没有明确设置值,则使用这些属性。然后,load 方法从名为defaultProperties的磁盘上的文件中读取默认值。

接下来,应用程序使用不同的构造函数创建第二个Properties对象applicationProps,其默认值包含在defaultProps中。当检索属性时,默认值起作用。如果在applicationProps中找不到属性,则会搜索其默认列表。

最后,代码从名为appProperties的文件中将一组属性加载到applicationProps中。该文件中的属性是上次调用应用程序时保存的属性,如下一节所述。

保存属性

以下示例使用Properties.store从前一个示例中写出应用程序属性。默认属性不需要每次保存,因为它们永远不会更改。

FileOutputStream out = new FileOutputStream("appProperties");
applicationProps.store(out, "---No Comment---");
out.close();

store方法需要一个要写入的流,以及一个字符串,该字符串用作输出顶部的注释。

获取属性信息

一旦应用程序设置了其Properties对象,应用程序可以查询该对象以获取有关其包含的各种键和值的信息。应用程序在启动后从Properties对象获取信息,以便根据用户的选择初始化自身。Properties类有几种获取属性信息的方法:

  • contains(Object value)containsKey(Object key)

    如果值或键在Properties对象中,则返回truePropertiesHashtable继承这些方法。因此,它们接受Object参数,但只应使用String值。

  • getProperty(String key)getProperty(String key, String default)

    返回指定属性的值。第二个版本提供默认值。如果找不到键,则返回默认值。

  • list(PrintStream s)list(PrintWriter w)

    将所有属性写入指定的流或写入器。这对于调试很有用。

  • elements()keys()propertyNames()

    返回一个包含Properties对象中包含的键或值(如方法名所示)的枚举。keys方法仅返回对象本身的键;propertyNames方法还返回默认属性的键。

  • stringPropertyNames()

    类似于propertyNames,但返回一个Set<String>,并且仅返回键和值都是字符串的属性名称。请注意,Set对象不由Properties对象支持,因此对其中一个对象的更改不会影响另一个对象。

  • size()

    返回当前键/值对的数量。

设置属性

用户在应用程序执行期间与应用程序的交互可能会影响属性设置。这些更改应该反映在Properties对象中,以便在应用程序退出时(并调用store方法时)保存这些更改。以下方法更改Properties对象中的属性:

  • setProperty(String key, String value)

    将键/值对放入Properties对象中。

  • remove(Object key)

    删除与键关联的键/值对。


注意: 上述描述的一些方法在Hashtable中定义,因此接受除String之外的键和值参数类型。始终使用String作为键和值,即使该方法允许其他类型。同时不要在Properties对象上调用Hashtable.setHastable.setAll;始终使用Properties.setProperty


命令行参数

原文:docs.oracle.com/javase/tutorial/essential/environment/cmdLineArgs.html

一个 Java 应用程序可以从命令行接受任意数量的参数。这允许用户在启动应用程序时指定配置信息。

用户在调用应用程序时输入命令行参数,并在要运行的类名后指定这些参数。例如,假设一个名为Sort的 Java 应用程序对文件中的行进行排序。要对名为friends.txt的文件中的数据进行排序,用户会输入:

java Sort friends.txt

当一个应用程序启动时,运行时系统会通过一个String数组将命令行参数传递给应用程序的主方法。在前面的示例中,传递给Sort应用程序的命令行参数是包含一个单独String的数组:"friends.txt"

回显命令行参数

Echo示例会将每个命令行参数单独显示在一行上:


public class Echo {
    public static void main (String[] args) {
        for (String s: args) {
            System.out.println(s);
        }
    }
}

以下示例展示了用户如何运行Echo。用户输入以斜体显示。

*java Echo Drink Hot Java*
Drink
Hot
Java

请注意,应用程序会将每个单词 — DrinkHotJava — 单独显示在一行上。这是因为空格字符分隔命令行参数。要将DrinkHotJava解释为单个参数,用户应该用引号将它们括起来。

*java Echo "Drink Hot Java"*
Drink Hot Java

解析数值型命令行参数

如果一个应用程序需要支持一个表示数字的命令行参数,它必须将代表数字的String参数(如"34")转换为数值。以下是一个将命令行参数转换为int的代码片段:

int firstArg;
if (args.length > 0) {
    try {
        firstArg = Integer.parseInt(args[0]);
    } catch (NumberFormatException e) {
        System.err.println("Argument" + args[0] + " must be an integer.");
        System.exit(1);
    }
}

如果parseIntargs[0]的格式无效,会抛出NumberFormatException。所有的Number类 — IntegerFloatDouble等等 — 都有parseXXX方法,将代表数字的String转换为其类型的对象。

环境变量

原文:docs.oracle.com/javase/tutorial/essential/environment/env.html

许多操作系统使用环境变量将配置信息传递给应用程序。与 Java 平台中的属性类似,环境变量是键/值对,其中键和值都是字符串。设置和使用环境变量的约定在操作系统之间以及命令行解释器之间有所不同。要了解如何在您的系统上将环境变量传递给应用程序,请参考系统文档。

查询环境变量

在 Java 平台上,应用程序使用System.getenv来检索环境变量的值。没有参数时,getenv返回一个只读的java.util.Map实例,其中映射键是环境变量名称,映射值是环境变量值。这在EnvMap示例中有所展示:


import java.util.Map;

public class EnvMap {
    public static void main (String[] args) {
        Map<String, String> env = System.getenv();
        for (String envName : env.keySet()) {
            System.out.format("%s=%s%n",
                              envName,
                              env.get(envName));
        }
    }
}

使用String参数,getenv返回指定变量的值。如果未定义变量,则getenv返回nullEnv示例以这种方式使用getenv来查询在命令行上指定的特定环境变量:


public class Env {
    public static void main (String[] args) {
        for (String env: args) {
            String value = System.getenv(env);
            if (value != null) {
                System.out.format("%s=%s%n",
                                  env, value);
            } else {
                System.out.format("%s is"
                    + " not assigned.%n", env);
            }
        }
    }
}

将环境变量传递给新进程

当 Java 应用程序使用ProcessBuilder对象创建新进程时,传递给新进程的默认环境变量集合与提供给应用程序的虚拟机进程的集合相同。应用程序可以使用ProcessBuilder.environment更改此集合。

平台依赖性问题

不同系统上实现环境变量的方式之间存在许多微妙的差异。例如,Windows 在环境变量名称中忽略大小写,而 UNIX 则不会。环境变量的使用方式也各不相同。例如,Windows 在名为USERNAME的环境变量中提供用户名,而 UNIX 实现可能在USERLOGNAME或两者中提供用户名。

为了最大化可移植性,在系统属性中提供相同值时,永远不要引用环境变量。例如,如果操作系统提供用户名,则始终可以在系统属性user.name中找到。

其他配置实用程序

原文:docs.oracle.com/javase/tutorial/essential/environment/other.html

这里是一些其他配置实用程序的摘要。

首选项 API允许应用程序在一个与实现相关的后备存储中存储和检索配置数据。支持异步更新,并且同一组首选项可以被多个线程甚至多个应用程序安全地更新。更多信息,请参考首选项 API 指南。

部署在JAR 归档中的应用程序使用清单来描述归档的内容。更多信息,请参考在 JAR 文件中打包程序课程。

Java Web Start 应用程序的配置包含在一个JNLP 文件中。更多信息,请参考 Java Web Start 课程。

Java 插件小程序的配置部分取决于用于在网页中嵌入小程序的 HTML 标签。根据小程序和浏览器,这些标签可以包括<applet><object><embed><param>。更多信息,请参考 Java 小程序课程。

java.util.ServiceLoader提供了一个简单的服务提供者设施。服务提供者是服务的实现—一组众所周知的接口和(通常是抽象的)类。服务提供者的类通常实现服务中定义的接口并子类化类。服务提供者可以作为扩展安装(参见扩展机制)。提供者也可以通过将它们添加到类路径或通过其他特定于平台的方式来提供。

系统工具

原文:docs.oracle.com/javase/tutorial/essential/environment/system.html

System 类实现了许多系统工具。其中一些已经在之前关于配置工具的部分中介绍过。本节介绍其他一些系统工具。

命令行 I/O 对象

原文:docs.oracle.com/javase/tutorial/essential/environment/cl.html

System 提供了几个预定义的 I/O 对象,这些对象在一个旨在从命令行启动的 Java 应用程序中非常有用。这些对象实现了大多数操作系统提供的标准 I/O 流,还有一个对于输入密码很有用的控制台对象。更多信息,请参考从命令行进行 I/O 在 基本 I/O 课程中。

系统属性

原文:docs.oracle.com/javase/tutorial/essential/environment/sysprop.html

在 Properties 中,我们研究了应用程序如何使用 Properties 对象来维护其配置。Java 平台本身使用 Properties 对象来维护自己的配置。System 类维护一个描述当前工作环境配置的 Properties 对象。系统属性包括当前用户、当前 Java 运行时版本以及用于分隔文件路径名组件的字符的信息。

以下表格描述了一些最重要的系统属性。

含义
"file.separator" 文件路径中分隔组件的字符。在 UNIX 上是 "/",在 Windows 上是 "\"。
"java.class.path" 用于查找包含类文件的目录和 JAR 存档的路径。类路径的元素由 path.separator 属性中指定的特定于平台的字符分隔。
"java.home" Java Runtime Environment (JRE) 的安装目录
"java.vendor" JRE 供应商名称
"java.vendor.url" JRE 供应商 URL
"java.version" JRE 版本号
"line.separator" 操作系统用于在文本文件中分隔行的序列
"os.arch" 操作系统架构
"os.name" 操作系统名称
"os.version" 操作系统版本
"path.separator" java.class.path 中使用的路径分隔符字符
"user.dir" 用户工作目录
"user.home" 用户主目录
"user.name" 用户账户名

安全注意事项: 访问系统属性可能受到 安全管理器 的限制。这在小程序中最常见,小程序被阻止读取某些系统属性,并且无法写入任何系统属性。有关在小程序中访问系统属性的更多信息,请参阅 System Properties 中的 Java 富互联网应用程序进阶 课程。


读取系统属性

System 类有两个用于读取系统属性的方法:getPropertygetProperties

System 类有两个不同版本的 getProperty。两者都检索参数列表中命名的属性的值。其中较简单的 getProperty 方法接受一个参数,即属性键。例如,要获取 path.separator 的值,请使用以下语句:

System.getProperty("path.separator");

getProperty 方法返回一个包含属性值的字符串。如果属性不存在,此版本的 getProperty 返回 null。

getProperty 的另一个版本需要两个 String 参数:第一个参数是要查找的键,第二个参数是在找不到键或键没有值时要返回的默认值。例如,下面的 getProperty 调用查找名为 subliminal.messageSystem 属性。这不是一个有效的系统属性,所以该方法返回提供的第二个参数作为默认值:“购买 StayPuft 棉花糖!

System.getProperty("subliminal.message", "Buy StayPuft Marshmallows!");

System 类提供的最后一个方法用于访问属性值的是 getProperties 方法,它返回一个 Properties 对象。该对象包含完整的系统属性定义集。

写入系统属性

要修改现有的系统属性集,请使用 System.setProperties。此方法接受一个已初始化以包含要设置的属性的 Properties 对象。此方法用新的由 Properties 对象表示的属性集替换整个系统属性集。


警告: 改变系统属性可能是潜在危险的,应谨慎操作。许多系统属性在启动后不会重新读取,并且仅用于信息目的。更改某些属性可能会产生意想不到的副作用。


下一个示例,PropertiesTest,创建一个 Properties 对象,并从 myProperties.txt 初始化它。

subliminal.message=Buy StayPuft Marshmallows!

PropertiesTest 然后使用 System.setProperties 将新的 Properties 对象安装为当前系统属性集。


import java.io.FileInputStream;
import java.util.Properties;

public class PropertiesTest {
    public static void main(String[] args)
        throws Exception {

        // set up new properties object
        // from file "myProperties.txt"
        FileInputStream propFile =
            new FileInputStream( "myProperties.txt");
        Properties p =
            new Properties(System.getProperties());
        p.load(propFile);

        // set the system properties
        System.setProperties(p);
        // display new properties
        System.getProperties().list(System.out);
    }
}

注意 PropertiesTest 如何创建 Properties 对象 p,并将其用作 setProperties 的参数:

Properties p = new Properties(System.getProperties());

此语句使用当前系统属性集初始化新的属性对象 p,在这个小应用程序的情况下,这是运行时系统初始化的属性集。然后应用程序从文件 myProperties.txt 中加载额外的属性到 p 中,并将系统属性设置为 p。这将导致将 myProperties.txt 中列出的属性添加到运行时系统在启动时创建的属性集中。请注意,应用程序可以创建没有任何默认 Properties 对象的 p,如下所示:

Properties p = new Properties();

还要注意系统属性的值是可以被覆盖的!例如,如果 myProperties.txt 包含以下行,则 java.vendor 系统属性将被覆盖:

java.vendor=Acme Software Company

一般来说,要小心不要覆盖系统属性。

setProperties 方法更改当前运行应用程序的系统属性集。这些更改不是持久的。也就是说,在应用程序内更改系统属性不会影响此应用程序或任何其他应用程序的将来调用 Java 解释器。运行时系统每次启动时都会重新初始化系统属性。如果要使系统属性的更改持久化,则应用程序必须在退出之前将值写入某个文件,并在启动时重新读取。