面向初学者的-Java-编程-全-

面向初学者的 Java 编程(全)

原文:zh.annas-archive.org/md5/4A5A4EA9FEFE1871F4FCEB6D5DD89CD1

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

无论您是第一次接触高级面向对象编程语言,比如 Java,还是已经有一段时间的编程经验,只是想要将 Java 添加到您的技能范围,或者您从未接触过一行代码,本书都旨在满足您的需求。我们将快速前进,不会回避繁重的主题,但我们将从最基础的知识开始,边学习面向对象编程的概念。如果这本书能帮助您理解 Java 编程的重要性,以及如何在 NetBeans 中开始开发 Java 应用程序,我将认为它是成功的。如果 Java 成为您最喜爱的编程语言,我同样会感到高兴!

您需要为本书做些什么

对于本书,您需要Java 开发工具包JDK)和 NetBeans

本书适合谁

本书适用于任何想要开始学习 Java 语言的人,无论您是学生、业余学习者,还是现有程序员想要增加新的语言技能。不需要 Java 或编程的任何先前经验。

约定

在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:"Source Packages文件夹是我们将编写代码的地方。"

代码块设置如下:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

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

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

任何命令行输入或输出都以以下方式编写:

java -jar WritingToFiles.jar

新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“选择 Java SE 列下方的下载按钮。”

警告或重要提示将显示在这样的框中。

提示和技巧会以这种方式出现。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对本书的看法,您喜欢或不喜欢的地方。读者的反馈对我们开发您真正受益的书籍至关重要。

要向我们发送一般反馈,只需发送电子邮件至feedback@packtpub.com,并在主题中提及书名。

如果您在某个专题上有专业知识,并且有兴趣编写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的自豪所有者,我们有一些事情可以帮助您充分利用您的购买。

下载示例代码

您可以从www.packtpub.com的帐户中下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,文件将直接通过电子邮件发送给您。您可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册到我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”选项卡上。

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

  4. 在搜索框中输入书名。

  5. 选择您要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的地方。

  7. 单击“代码下载”。

文件下载完成后,请确保您使用最新版本的解压缩软件解压文件夹:

  • Windows 系统使用 WinRAR / 7-Zip

  • Mac 系统使用 Zipeg / iZip / UnRarX

  • Linux 系统使用 7-Zip / PeaZip

本书的代码包也托管在 GitHub 上,链接为github.com/PacktPublishing/Java-Programming-for-Beginners。我们还有其他丰富图书和视频代码包,可在github.com/PacktPublishing/找到。快去看看吧!

下载本书的彩色图片

我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。这些彩色图片将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/JavaProgrammingforBeginners_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽最大努力确保内容的准确性,但错误还是会发生。如果您在我们的书中发现错误——可能是文字或代码上的错误,我们将不胜感激,如果您能向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata报告,选择您的书,点击“勘误提交表”链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该书标题的勘误部分的任何现有勘误列表中。

要查看先前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。所需信息将出现在“勘误”部分下。

盗版

互联网上的版权盗版是所有媒体的持续问题。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。

请通过copyright@packtpub.com与我们联系,并附上涉嫌盗版材料的链接。

感谢您帮助保护我们的作者,以及我们为您提供有价值内容的能力。

问题

如果您在阅读本书的过程中遇到任何问题,可以通过questions@packtpub.com与我们联系,我们将尽力解决。

第一章:开始使用 Java

无论这是你第一次涉足高级面向对象的编程语言,比如 Java,还是你已经编程了一段时间,只是想要把 Java 加入你的技能库,甚至你一生中从未接触过一行代码,这本书都是为你设计的。我们将快速前进,不会回避繁重的主题;然而,我们将从最基础的知识开始,随着学习面向对象编程的概念。

在本章中,我们将了解 Java 是什么,以及它的特性。然后,我们将按步骤设置开发环境,使我们能够编写和执行 Java 程序。一旦我们完成这一步,我们将编写我们的第一个 Java 程序并运行它。最后,我们将看看当我们遇到错误时该怎么办。

具体来说,我们将涵盖以下主题:

  • 什么是 Java

  • Java 的特性和应用

  • 安装 JDK

  • 安装 NetBeans IDE

  • 编写HelloWorld.java

  • NetBeans 的错误检测能力

什么是 Java?

Java 是由 Sun Microsystems 于 1995 年开发的,但它经受住了时间的考验,至今仍然非常相关和广泛使用。那么 Java 究竟是什么?Java 是一种高级的、通用的面向对象的编程语言。

Java 的特性

以下是 Java 的主要特性:

  • 高级和通用:Java 不是为了完成一个非常特定的任务而创建的,而是允许我们在一个开放的环境中编写计算机可读的指令。因为每台计算机系统都有自己专门的编程语言并不现实,甚至不可取,所以绝大多数代码都是用高级通用语言编写的,比如 Java。

  • 面向对象:Java 也是我们所说的面向对象的语言。虽然我们在本书的后面才会深入讨论对象和类的具体内容,但现在知道对象允许我们在程序中定义模块化实体,使它们更易于阅读和更易于创建大规模的软件项目。对面向对象概念的牢固掌握对于任何现代软件开发人员来说绝对是必不可少的。

  • 平台无关:最后,Java 的设计初衷是成为一种一次编写,随处运行的语言。这意味着如果你和我都有安装了 Java 的系统,即使我们的系统通常不相同--例如,我用的是 Windows 系统,你用的是 Mac--我在我的机器上运行的 Java 程序,我给你,也会在你的机器上基本上相同地运行,而无需重新编译。

编译Java 等编程语言是将我们编写的人类可读代码转换为解释的机器友好代码的行为。不幸的是,这通常对人类来说并不友好。为了做到这一点,我们使用一个称为编译器的程序,它接收我们的代码作为文本,并将其转换为机器代码。

传统上,我们必须为每个要运行的系统重新编译程序,因为所有系统对其机器代码的理解都不同。Java 通过将所有 Java 程序编译为一种称为字节码的相同类型的解释代码来避免这个问题。

字节码中的编译 Java 程序可以在安装了 Java 的任何系统上运行。这是因为当我们在您的系统上安装 Java 时,我们还会安装一个特定于该系统的 Java 虚拟机。这台机器的责任是将字节码转换为最终发送到该系统处理器的指令。

通过使系统负责进行最终转换,Java 创造了一种一次编写,随处运行的语言,我可以把一个 Java 程序交给你,你可以在你的计算机上运行它,而且相当肯定它会以与我的计算机上相同的方式运行。这种强大的跨平台支持水平使得 Java 成为软件开发世界的主要工具之一。

Java 应用程序

在当今的现代时代,Java 被用于开发桌面应用程序、Web 服务器和客户端 Web 应用程序。它是 Android 操作系统的本地语言,可以在 Android 手机和平板电脑上运行。

Java 已被用于编写视频游戏,有时甚至被移植到没有传统操作系统的较小设备上。它仍然是当今技术世界中的一个重要角色,我期待与您一起学习它。

设置您的开发环境

在本节中,我们将编写我们的第一个 Java 程序,但在我们开始编码之前,我们需要设置一个友好的 Java 开发环境。

安装 JDK

要开始这个过程,让我们下载一个Java 开发工具包JDK)或 Java SDK。这个工具包包含允许我们用 Java 代码做很多不同事情的库和可执行文件。最重要的是,安装了我们的 SDK 后,我们将能够编译 Java 代码,然后运行已完成的 Java 程序。

您可能已经在您的计算机上安装了 Java;但是,除非您明确地这样做,您可能还没有安装 Java SDK。普通用户在其计算机上安装的 Java 版本称为Java 运行环境JRE)。这允许执行 Java 程序,并且没有安装 JRE 的环境中无法运行 Java 程序。但是 JRE 不包含任何真正的开发工具,而这是我们需要的。好消息是 Java JRE 和 Java SDK 可以和谐共存。Java JRE 实际上只是 SDK 的一个子集,因此如果我们只安装了即将下载的 Java 开发工具包,我们就没问题了。

如果您以前已经下载了 Java 开发工具包,当您实际安装这个工具包时,Java 会让您知道它已经安装了,您可以跳过本节的这部分。对于其他人,请查看如何下载开发工具包:

  1. 首先,通过浏览器导航到www.oracle.com/technetwork/java/javase/downloads/index.html

  2. 我们将使用由 Oracle 维护的 Java SE 或标准版开发工具包。要获取此工具包,只需转到“下载”选项卡,并选择我们想要 JDK:

向下滚动,查看许可协议,接受许可协议,然后下载适合您操作系统的 SDK 版本。对我来说,这是jdk-8u144-windows-x64.exe,列在最后。

  1. 一旦您的下载完成,安装它就像我们安装其他程序一样。在适当的时候选择默认选项,并确保记下我们将安装开发工具包的目录。

安装 NetBeans IDE

安装了我们的 Java 开发工具包,我们从技术上讲已经拥有了开始编写 Java 程序所需的所有工具。但是,我们必须通过命令行来编译它们,这在不同的操作系统上可能看起来有些不同。

为了保持一切简单,让我们通过在集成开发环境IDE)中编写 Java 代码来开始学习 Java。这是一个独立的软件程序,可以帮助我们编写、编译和运行 Java 程序。我们将使用 NetBeans IDE,这很棒,因为它是免费的、开源的,并且在 Windows、Mac 和 Linux 环境中运行几乎相同。

要获取这个 IDE,前往netbeans.org/downloads/

您将看到以下页面:

因为我们已经下载了 Java 标准版开发工具包,所以这里我们要下载的是 NetBeans 的 Java SE 版本。选择“Java SE”列下面的下载按钮。NetBeans 应该会自动开始下载,但如果没有,点击以下图片中显示的链接:

再次,我们将像安装任何其他程序一样安装 NetBeans,在适当的时候选择默认选项。很可能,NetBeans 会在我们的计算机上找到 Java 开发工具包。如果没有,它会提示我们安装 Java 开发工具包的目录。

编写我们的第一个 Java 程序

希望您已经安装了 NetBeans,并且没有遇到任何麻烦就启动了它。NetBeans 会管理我们程序的文件结构,但首先,我们需要告诉 NetBeans 我们准备开始一个新项目。

创建一个新项目

要创建一个新项目,点击“文件”,然后“新建项目”,选择 Java 应用程序:

我们需要给我们的项目一个独特的名称;让我们称这个为HelloWorld。然后,我们可以选择一个放置文件的位置。因为这是我们的第一个 Java 程序,我们可能应该尽可能地从零开始。所以让我们取消选中“创建主类”选项,这样 NetBeans 会给我们一个几乎是空白的项目。然后,点击“完成”:

NetBeans 会为我们设置一个文件系统。我们可以像在标准文件系统资源管理器中一样浏览这个文件系统:

Source Packages文件是我们将编写代码的地方。您会注意到在Libraries文件下,JDK 是链接的,允许我们访问其许多库资源:

创建一个 Java 类

创建一个新项目后,我们应该看到我在下面的图片中看到的项目、文件和服务选项卡。让我们看看文件选项卡。虽然项目选项卡有点抽象,但文件选项卡显示了我们的HelloWorld项目所在的文件系统中实际包含的内容:

最重要的是,这里的src文件没有任何文件。这是因为我们的项目没有与之关联的源代码,所以现在它不会做任何事情。为了解决这个问题,右键单击src,选择“新建”,然后选择“Java 类...”:

我们将把我们的 Java 类命名为HelloWorld,就像项目的名称一样,因为这是我们的主类,程序应该从这里输入和开始。其他的东西现在都应该正常工作,所以点击“完成”,NetBeans 会为我们创建HelloWorld.java。一个.java文件本质上是一个文本文件,但它应该只包含 Java 代码和注释:

编写代码

当我们告诉 NetBeans 创建HelloWorld.java文件时,它已经为我们添加了一些代码,如下面的截图所示:

Java 注释

您会注意到这个文档的一些内容是完全可读的;这些就是我们所谓的注释。在 Java 文件中出现在/**/符号之间的任何文本都将被编译器完全忽略。我们可以在这里写任何我们想要的东西,它不会影响我们的程序如何运行。现在,让我们删除这些注释,这样我们就可以纯粹地处理我们的 Java 代码。

main()函数

Java 代码,就像英语一样,是从上到下,从左到右阅读的。即使我们的项目包含许多文件和许多类,我们仍然需要从特定点开始阅读和执行我们的代码。我们将这个文件和类命名为HelloWorld,与我们的项目同名,因为我们希望它是特殊的,并包含public static void main(String[] args)方法,我们的代码执行将从这里开始。这是一个很啰嗦的行话。现在,只需将其输入并知道这是我们的 Java 程序开始阅读和执行的地方。main()函数的代码用大括号括起来:

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

在 IDE 中工作的一个很棒的地方是它会突出显示哪些括号相互对应。括号允许我们将代码放在其他代码区域中。例如,我们的main()方法包含在HelloWorld类中,我们即将编写和执行的 Java 代码将包含在我们的main()方法中。目前什么都没有的第 4 行是我们的程序将开始阅读和执行 Java 代码的地方。

打印字符串

我们的HelloWorld程序的目标相当温和。当它运行时,我们希望它将一些文本打印到屏幕底部的输出框中。

当我们下载了 Java SDK 时,我们获得了一个有用函数库,其中一个函数将做到这一点。这就是println(),或者打印行,函数。当我们的 Java 代码执行这个函数时,它会立即执行,因为它是我们main()方法入口点中的第一个函数,Java 代码将向我们的输出框打印一些文字。函数名后面跟着开括号和闭括号。在这些括号内,我们放置函数完成任务所需的信息。println()方法当然需要知道我们想要它打印什么。在 Java 中,一行文本由两个双引号括起来,我们称之为字符串。让我们让我们的程序打印"Hello World!"

Java 语法

你可能已经注意到 NetBeans 一直在对我们大声呼喊。左边有一个灯泡和一个红点,文本下面有一些红色的抖动,很像在一些文本编辑器中出现拼写错误。这确实是我们所做的。我们犯了一个语法错误。我们的 Java 代码显然有问题,NetBeans 知道这一点。

这里有两个问题。首先是我们的代码没有以分号结束。Java 不能很好地读取空格和换行,所以我们需要在每行功能代码的末尾加上分号,原因与摩尔斯电码操作员在每行末尾发送消息“停止”是一样的。让我们在我们的println()语句的末尾添加一个分号:

NetBeans 变得更加满意了;抖动减少了,但如前面的截图所示,仍然有些问题。

问题在于编程语言中的函数,就像计算机上的文件一样,有一个存在的位置。NetBeans 不确定在哪里找到我们尝试使用的println()函数。所以我们只需要告诉 NetBeans 这个函数存在的位置。println()函数的完整路径始于System包,其中包括out类,该类定义了println()函数。我们在 Java 中写成System.out.println("Hello World!");,如下面的代码块所示。

让我们去掉我在第 5、6 和 7 行创建的额外空格,不是因为它们会影响我们程序的运行方式,而是因为这样看起来不够好看。现在我们已经写好了我们的HelloWorld程序:

public class HelloWorld { 
    public static void main(String[] args) { 
        System.out.println("Hello World!"); 
    } 
} 

执行我们的程序

那我们该怎么办呢?正如我们所知,我们的计算机无法直接阅读这段 Java 代码。它必须将其转换为计算机可读的语言。因此,执行这段代码变成了一个两步过程:

  1. 编译我们的程序:首先,我们要求 NetBeans 构建我们的项目。这意味着项目中的所有代码将被编译并转换为计算机可读的代码,基本上是一个计算机可读的项目!

当我们按下“构建项目”按钮时,屏幕底部的输出框中会显示一大堆文本--希望是友好的“构建成功”消息,然后是构建项目所花费的时间:

  1. 运行我们的程序:一旦我们构建了我们的项目,我们可以按下“运行项目”按钮来执行我们的代码和我们的println语句:

然后 NetBeans 会给我们以下弹出框:

当我们在 IDE 之外执行程序时,我们通过启动其中一个可执行文件来执行它。因为我们现在处于集成开发环境中,NetBeans 想要确定我们希望将我们的哪个文件作为程序的入口点。我们只有一个选择,因为我们只写了一个 Java 类。所以让我们向 NetBeans 确认HelloWorld是我们的主类,因此HelloWorld程序中的main()函数将是我们开始执行 Java 程序的地方。然后,当我们点击“确定”时,输出框将告诉我们程序已经开始运行,然后我们的程序会像我们预期的那样在输出框中打印"Hello World!"

就是这样!现在我们是 Java 程序员了。当然,还有很多要学习的。事实上,Java 中的HelloWorld可能是你写过的最简单的程序。Java 非常强大,事实上我们在写第一个程序时根本无法希望理解它的所有复杂性。真正的好消息是,从这一点开始,我们需要更少的信仰飞跃,我们可以开始逐步建立对 Java 的非常扎实的理解。

如何解释 NetBeans 检测到的错误?

随着我们编写越来越复杂的 Java 程序,我们不可避免地会犯一些错误。其中一些错误将是重大的逻辑错误或者是我们的误解,我们可能需要进一步教育自己才能解决。但是,特别是在开始编程时,我们会犯很多小错误,只要我们知道在哪里找,这些错误就非常容易修复。

幸运的是,Java 编译器设计成在遇到错误时向我们指出错误。为了看到这一点,让我们简单地使我们的HelloWorld程序不正确,方法是删除println语句末尾的分号:

现在 NetBeans 会在错误的行上显示红色波浪线,以让我们知道它相当确定有些地方出错了,但是我们仍然可以让编译器试一试。如果我们尝试构建这个项目,我们将不会得到通常的编译成功消息;相反,我们会得到一个错误消息:

这个错误是“需要';'”,这是一个非常方便和自解释的错误消息。同样重要的是这条消息后面的数字,是4。这让我们知道编译器在哪一行遇到了这个错误。在 NetBeans 中,如果我们点击错误消息,IDE 将会突出显示该行代码:

如果我们加入分号,那么我们的程序将成功构建,如下面的屏幕截图所示:

这就是全部内容。

当然,并非所有的错误消息都是那么自解释的。举例来说,让我们创建一个稍微复杂一点的错误。如果在这个程序中我们忘记插入一个括号会发生什么?这在下面的代码中有所说明:

当我们按下构建项目时,我们得到了两个错误,尽管我们只犯了一个错误:

我们的第一个错误是not a statement,然后它告诉我们它不理解的那一行。如果我们仔细看一下第一个错误,我们可能会注意到我们缺少一对括号,所以我们将能够修复这个错误;但是,第二个错误呢?我们再次得到了';' expected,尽管在这种情况下我们确实有一个分号。

嗯,一旦程序中发生了一个错误,编译器理解代码行的能力就会很快破裂。当我们调试我们的代码时,一个基本的经验法则是只处理列表中的顶部错误;那是编译器在我们的代码中遇到的第一个错误。我们可能能够从下面更多的错误中获得一些有用的信息,但更多的情况是,它们只是由我们第一个语法错误生成的错误。这里没有什么太惊人的,但我想向你指出这一点,因为能够追踪编译器错误可以在我们学习编程时节省我们很多麻烦。

代码补全功能

在谈论 NetBeans 时,让我们快速了解另一个 IDE 功能。假设我想写一行新代码,我要使用System库中的某个东西:

一旦我输入System.,NetBeans 就可以为我建议有效的响应。当然,其中只有一个是我要找的。NetBeans 编译器有很多这样的有用功能。如果你是那种认为代码补全很棒的人,可以继续使用这些工具。我们可以通过转到工具 | 选项 | 代码补全,并勾选我们想要的功能来实现这一点:

如果你更希望 NetBeans 的行为更像文本编辑器,可以取消所有功能的勾选。

我们开始吧,在这一节中有很多清理工作,但希望会很快,也不会太痛苦。

总结

在本章中,您了解了 Java 是什么,以及它的特点。我们通过查看它在各个领域的应用来看到了 Java 的广泛应用。

我们走过了安装 Java 开发工具包的步骤。然后设置了一个名为NetBeans的开发环境,用于编写和执行 Java 程序。我们看到了如何使用 NetBeans 并在其中编写了我们的第一个 Java 程序。接下来,我们看到了如何使用 NetBeans 的错误检测功能来纠正错误。

在下一章中,我们将看一下各种 Java 数据类型以及如何使用变量。

第二章:理解有类型的变量

要创建甚至是简单的 Java 程序,我们需要一种存储和操作信息的方法。在这种情况下,我们的主要资源是变量,这就是我们将在本章中讨论的内容。我们将看看 Java 中的不同数据类型以及如何在程序中使用它们。我们还将看到Math类库及其一个函数。

具体来说,我们将讨论以下主题:

  • 变量的介绍及其必要性

  • 整数变量

  • 浮点变量

  • Math类库及其pow()函数

  • 字符变量

  • String类及其方法

整数变量

首先,让我们在 NetBeans 中创建一个新项目。我将把我的称为Variables,这次我们将允许 NetBeans 为我们创建主类,以便我们尽快开始编码。我们需要删除 NetBeans 在创建新项目时自动创建的所有注释,以便尽可能保持一切清晰可读,然后我们就可以开始了:

最初的计算机只不过是计算器,当然,Java 保留了这种功能。例如,Java 可以计算1+1,结果当然是2。然而,Java 相当复杂,设计用于执行许多不同的任务,因此我们需要为我们的命令提供上下文。在这里,我们告诉 Java 我们希望它打印1+1的结果:

package variables; 

public class Variables { 

    public static void main(String[] args) { 
        System.out.println(1+1); 
    } 

} 

我们之前的程序将如预期般运行:

除了其他一些操作,Java 可以执行所有基本的算术运算。它可以进行加法、减法、乘法(我们使用*,而不是键盘上的X),以及除法。如果我们运行以下程序并输入23,我们将看到四个println()命令,所有这些命令都将给出正确的计算结果。当然,我们可以将这些数字更改为任何我们认为合适的数字组合:

package variables; 

public class Variables { 

    public static void main(String[] args) { 
        System.out.println(2+3); 
        System.out.println(2-3); 
        System.out.println(2*3); 
        System.out.println(2/3); 
    } 
} 

以下是前面代码的输出:

手动更改这些行有点麻烦,如果我们编写非常复杂的程序或接受用户输入的动态程序,这很快就变得不可行。

变量的解决方案

幸运的是,编程给了我们一种存储和检索数据的方法;这就是变量。要在 Java 中声明一个变量,我们首先必须指定我们将使用的变量类型。变量有许多不同的类型。在这种情况下,我们满足于使用整数,即没有指定小数位并且不是分数的数字。此外,在这种情况下,使用 Java 的原始类型是合适的。这些基本上是 Java 编程语言中信息的基本级别;我们在 Java 中使用的几乎所有其他东西都是由原始类型构建的。

要声明整数原始类型的变量,即整数,我们使用int关键字,全部小写。一旦我们这样做,我们需要给我们的变量一个名称。这是一个唯一的标识符,我们将用它来在将来访问这个信息。我们本地程序中的每个变量都应该有自己的名称。让我们称我们的第一个变量为x,我们的第二个变量为y

package variables; 

public class Variables { 

    public static void main(String[] args) { 
        int x; 
        int y; 

        System.out.println(2+3); 
        System.out.println(2-3); 
        System.out.println(2*3); 
        System.out.println(2/3); 
    } 
} 

我们刚刚编写了两行完全合法的 Java 代码。如果我们现在运行程序,我们将看到与之前相同的输出:

然而,在幕后,Java 也会为我们的xy变量设置内存空间。这种分配不会影响我们的println命令,因为变量在其中还没有被引用。

所以让我们在变量中存储一些信息。我们可以在创建变量后通过变量的名称来引用变量。重要的是我们不要再次键入int x来引用我们的变量,因为这是 Java 创建一个全新变量x而不是访问现有变量x的命令:

一旦我们引用了变量,我们就可以使用等号更改其值。所以让我们将x设置为4y设置为3。我们的println命令目前使用两个明确声明的整数:数字23。由于xy也是整数,我们可以简单地用变量xy替换现有的数字:

package variables; 

public class Variables { 

    public static void main(String[] args) { 
        int x; 
        int y; 
        x = 4; 
        y = 3; 
        System.out.println(x+y); 
        System.out.println(x-y); 
        System.out.println(x*y); 
        System.out.println(x/y); 
    } 
} 

以下是前述代码的输出:

当我们的 Java 代码涉及变量xy时,它将查看它们当前具有的整数值。它会找到数字43。因此,如果我们运行程序,我们应该期望第一个println语句x+y计算为4+3,然后计算为7。这正是发生的事情。

所以这里有一些有趣的事情。我们程序的最后一行,其中我们将x除以y,并没有像我们在数学上期望的那样进行评估。在这行代码中,x的值为4y的值为3,现在4除以3等于 1.3,但我们的程序只是输出1。那是因为 1.3 不是有效的整数值。整数只能是整数,永远不是分数或小数。因此,为了让我们使用整数,Java 会将具有小数部分的任何计算向下舍入到最接近的整数。如果我们想要在可能有分数结果的环境中工作,我们需要使用除整数以外的原始类型。

无论如何,现在我们已经设置了我们的println命令以接受整数变量输入而不是明确的数字,我们可以通过简单地更改这些整数变量的值来修改所有四行计算的行为。例如,如果我们希望我们的程序在输入值-105(整数可以是负数;它们只是不能有分数部分)上运行,我们只需要更改我们给变量xy的值:

package variables; 

public class Variables { 

    public static void main(String[] args) { 
        int x; 
        int y; 

        x = -10; 
        y = 5; 

        System.out.println(x+y); 
        System.out.println(x-y); 
        System.out.println(x*y); 
        System.out.println(x/y); 
    } 
} 

如果我们快速运行前述代码,我们将看到预期的结果:

太棒了!您刚刚学会了在 Java 中使用整数和变量的基础知识。

整数变量的内存分配

让我们来看一个边缘情况,并了解一下 Java 的思维方式。您可能还记得我之前提到过,Java 在声明新变量时会设置内存。这是在高级编程语言(如 Java)中工作的巨大优势之一。Java 为我们抽象化或自动处理大部分内存管理。这通常使编写程序更简单,我们可以编写更短、更干净和更易读的代码。当然,重要的是我们要欣赏幕后发生的事情,以免遇到问题。

例如,每当 Java 为整数变量设置内存时,它也为所有整数变量设置相同数量的内存。这意味着 Java 可能在整数变量中存储的最大和最小值。最大整数值为2147483647,最小整数值为2147483648

那么让我们做一个实验。如果我们尝试存储并打印一个比最大值大一的整数变量会发生什么?首先,让我们简化我们的程序。我们只是将一个比可能的值高一的值分配给变量x

当我们尝试这样做时,NetBeans 会对我们大喊大叫。它内置了一些逻辑,试图阻止我们犯下这个非常基本和常见的错误。如果我们尝试编译这个程序,我们也会得到一个错误。

然而,我们想要以科学的名义犯这个错误,所以我们要欺骗 NetBeans。我们将把变量x的值设置为最大可能的整数值,然后在我们的代码的下一行,我们将把x的值设置为比当前x高一的值,也就是x=x+1。实际上,我们可以使用一个巧妙的简写:x=x+1等同于x++。好的,当我们运行这个程序时,它将欺骗编译器和 NetBeans,并在运行时进行加法运算,我们尝试打印出一个整数值,这个值比 Java 可以存储在内存位置中的最大整数值多一:

package variables; 

public class Variables { 

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

        x = 2147483647; 

        x++; 
        System.out.println(x); 

    } 
} 

当我们运行上述程序时,我们会得到以下负数:

这个数字恰好是我们可以存储在整数中的最小数字。这在视觉上有一定的意义。我们已经走得如此之远,或者说向右,在我们的整数数线上,以至于我们到达了最左边或最负的点。当然,在数学上,这可能会变得相当混乱。

我们不太可能编写需要比这个值更高的整数的程序。然而,如果我们确实需要,我们当然需要意识到这个问题并规避它,使用一个可以处理更大值的变量类型。long变量类型就像整数一样,但我们需要为它分配更多的内存:

package variables; 

public class Variables { 

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

        x = 2147483647; 

        x++; 
        System.out.println(x); 

    } 
} 

当我们运行上述程序时,我们将得到一个数学上准确的结果:

浮点变量

当我们只是简单地计数和操作整个对象时,整数是很棒的。然而,有时我们需要以更数学的方式处理数字,我们需要一个数据类型,可以让我们表达不完全是整数的想法。浮点数,或者浮点数,是 Java 的一个原始类型,允许我们表示有小数点和分数的数字。在本节中,我们将修改一些浮点和整数变量,以便看到它们的相似之处和不同之处。

让我们创建一个新的 Java 项目(你现在已经知道了)并将其命名为FloatingPointNumbers。让我们首先声明两个变量:一个整数(iNumber)和一个浮点数(fNumber)。正如我们所知,一旦声明了这些变量,我们就可以在我们的 Java 程序中修改和赋值给它们。这一次,让我向你展示,我们也可以在声明这些变量的同一行中修改和赋值给这些变量。所以当我声明了我的iNumber整数变量时,我可以立即给它赋值5

package floatingpointnumbers; 

public class FloatingPointNumbers { 

    public static void main(String[] args) { 
        int iNumber = 5; 
        float fNumber; 
    } 
} 

请注意,如果我们尝试用我们的浮点变量做类似的事情,NetBeans 会对我们大喊大叫,左侧会显示一个灯泡和红点:

实际上,如果我们尝试编译我们的程序,我们会得到一个合法的编译器错误消息:

让我们分析一下为什么会发生这种情况。当我们在 Java 中使用一个显式数字,也就是说,打出数字而不是使用一个变量时,Java 仍然会给这个显式数字一个类型。因此,当我们打出一个没有小数位的数字时,这个数字被假定为整数类型。所以我们的赋值工作得很好。然而,带有小数位的数字被假定为这种类型,称为double;它是float数据类型的姐妹类型,但并不完全相同。我们稍后会讨论double。现在,我们需要告诉 Java 将5.5视为float类型的数字,而不是double。为此,我们只需要在数字后面加上f,如下所示:

float fNumber = 5.5f; 

你会发现灯泡和红点已经消失了。为了确保我们的语法正确,让我们给我们的程序一些超级基本的功能。让我们使用System.out.println()按顺序打印我们的整数和浮点数变量:

System.out.println(iNumber); 
System.out.println(fNumber); 

当我们构建这个程序时,我们的编译器错误消失了,当我们运行它时,我们看到了两个分配的值,一切都如预期那样。没有什么太激动人心的地方:

整数和浮点数据类型之间的行为差异

现在,我们不再为变量分配显式值,而是进行一些基本的算术运算,以便我们可以看到在 Java 中修改整数和浮点数时的不同行为。在 Java 中,floatint都是原始类型,是编程语言的逻辑构建块。这意味着我们可以使用数学运算符进行比较和修改,例如除法。

我们知道,如果我们尝试将一个整数除以另一个整数,我们总是会得到一个整数作为结果,即使标准数学规则并不产生预期的结果。然而,如果我们将一个浮点数除以另一个浮点数,我们将得到一个更符合数学规则的结果:

package floatingpointnumbers; 

public class FloatingPointNumbers { 

    public static void main(String[] args) { 
        int iNumber = 5/4; 
        float fNumber = 5.0f/4.0f; 
        System.out.println(iNumber); 
        System.out.println(fNumber); 
    } 
} 

以下是前面代码的输出:

有时,Java 会让我们做一些可能不是那么好的主意的事情。例如,Java 允许我们将浮点变量fNumber的值设置为一个整数除以另一个整数,而不是一个浮点数除以另一个浮点数:

int iNumber = 5/4; 
float fNumber = 5/4; 

因为等号右侧的计算发生在我们的浮点变量fNumber的值改变之前,所以我们将在5/4的计算中看到相同的输出。这是因为 5 和 4 都是整数变量。因此,当我们运行程序时,即使fNumber仍然是一个浮点数(因为它带有小数点),它的值仍然设置为5/4的向下取整整数部分:

解决这个问题非常简单;我们只需要将我们的整数值之一更改为浮点数,通过在其后添加f

int iNumber = 5/4; 
float fNumber = 5/4.0f; 

现在计算将知道如何进行小数点的除法:

当我们停止使用显式声明的数字并开始使用变量时,正确地导航这一点变得更加棘手和重要。

现在让我们声明两个整数变量。我只是称它们为iNumber1iNumber2。现在,我们不再试图将fNumber的值设置为一个显式声明的数字除以另一个数字,而是将其值设置为iNumber1/iNumber2,然后我们将打印出存储在fNumber中的结果:

package floatingpointnumbers; 

public class FloatingPointNumbers { 

    public static void main(String[] args) { 
        int iNumber1 = 5; 
        int iNumber2 = 6; 
        float fNumber = iNumber1/iNumber2; 

        System.out.println(fNumber); 
    } 
} 

当我们运行这个程序时,因为我们再次将一个整数除以另一个整数,我们将看到向下取整的现象。存储在我们的浮点变量中的值是0.0,即5/6的向下取整结果:

如果我们正在处理显式声明的数字,我们可以通过将两个整数数字中的一个更改为浮点数来解决这个问题,只需在其后加上小数点和f。在这种情况下,使用iNumber2f不是一个选择,因为 Java 不再认为我们要求它将iNumber2视为浮点数,而是认为它正在寻找一个名为iNumber2f的变量,而这在这个上下文中显然不存在。

类型转换

我们也可以通过使用所谓的转换来实现类似的结果。这是一个命令,我们要求 Java 将一个类型的变量视为另一个类型。在这里,我们绕过了 Java 自然倾向于将iNumber1iNumber2视为整数的倾向。我们介入并说:“你知道 Java,把这个数字当作浮点数处理”,当我们这样做时,我们承担了一些责任。Java 会尝试按照我们的要求做,但如果我们选择不当并尝试将一个对象转换为它不能转换的对象,我们的程序将崩溃。

幸运的是,我们在这里使用的是原始类型,原始类型知道如何像另一种类型一样行事。因此,我们可以通过将变量iNumber1转换为浮点数来实现类似的结果,方法是在其前面加上(float)

float fNumber = (float)iNumber1/iNumber2; 

现在,如果我们运行我们的程序,我们将看到预期的5/6结果:

这是一个非常扎实的关于使用浮点数的介绍,我们几乎在任何时候都会使用它们来处理数学意义上的数字,而不是作为整数来计算整个对象。

双精度数据类型

让我们简要谈谈double数据类型。它是float的姐妹类型。它提供更高的分辨率:double数字可以有更多的小数位。但它们占用了更多的内存。在这个时候,使用 double 或 float 几乎总是一个风格或个人偏好的决定。除非你正在处理必须以最高内存效率运行的复杂软件,否则双精度占用的额外内存空间并不是非常重要的。

为了说明double的工作原理,让我们将FloatingPointNumbers.java程序中的两个整数更改为double数据类型。当我们只更改变量的名称时,程序的逻辑并没有改变。但是当我们将这些变量的声明从整数更改为双精度时,逻辑确实发生了变化。无论如何,当我们显式声明带有小数位的数字时,默认为double

现在我们需要修复错误。错误是因为将double数据类型除以另一个double数据类型将返回一个double结果。我们可以通过两种方式解决这个问题:

  1. 首先,我们可以将dNumber1dNumber2转换为浮点数,然后再将它们相除:
float fNumber = (float) dNumber1/ (float) dNumber2; 
  1. 然而,将我们的两个双精度数字相除是一个完全合法的操作。那么为什么不允许这种自然发生,然后将结果的双精度转换为浮点数,从而保留更多的分辨率。就像在代数中一样,我们可以使用括号将我们希望在另一个块之前发生的程序的概念块分解:
float fNumber = (float) (dNumber1/dNumber2); 

现在如果我们运行这个程序,我们会得到预期的结果:

Math 类库

在任何软件开发项目中,我们将花费大量时间教导我们的程序解决它经常遇到的问题类型。作为程序员,我们也会一次又一次地遇到某些问题。有时,我们需要编写自己的解决方案,并希望将它们保存以备将来使用。然而,更多的时候,有人之前遇到过这些问题,如果他们已经公开提供了解决方案,我们的一个选择就是利用他们的解决方案来获益。

在这一部分,我们将使用与 JDK 捆绑在一起的Math类库来解决一些数学问题。要开始这一部分,创建一个全新的 NetBeans 项目(我将其命名为TheMathLib)并输入main()函数。我们将编写一个非常简单的程序。让我们声明一个浮点数变量并给它一个值(不要忘记在我们显式数字的末尾加上f字母,让 Java 知道我们声明了一个浮点数),然后使用System.out.println()将这个值打印到屏幕上:

package themathlib; 

public class TheMathLib { 
    public static void main(String[] args) { 
        float number = 4.321f; 
        System.out.println(number); 
    } 
} 

好的,我们到这里:

现在,通过这个程序,我们希望能够轻松地将我们的浮点数提高到各种幂。所以,如果我们只想将这个数字平方,我想我们可以直接打印出number*number的值。如果我们想将其立方,我们可以打印出number*number*number。如果我们想将它提高到 6 次幂,我们可以将它乘以自身六次。当然,这很快就会变得难以控制,肯定有更好的方法。

让我们利用 Java 的Math类库来帮助我们将数字提升到不同的指数幂。现在,我刚告诉你我们正在寻找的功能存在于Math类库中。这是你应该期望从 Google 搜索中得到的正确方向,或者如果你是一名经验丰富的软件开发人员,你可以实现一个特定的 API。不幸的是,这对我们来说还不够信息来开始使用这个类库的功能。我们不知道它的工作细节,甚至不知道它为我们提供了什么功能。

要找出这个,我们需要查看它的文档。这是由 Oracle 管理的 Java 开发工具包中的库的文档网页:docs.oracle.com/javase/7/docs/api/。在页面上显示的库中,有java.lang。当我们选择它时,我们会在类摘要下找到我们一直在寻找的Math类。一旦我们导航到Math类库页面,我们会得到两件事。首先,我们得到一些关于库的人性化文本描述,它的历史,它的预期用途,非常元级别的东西。如果我们向下滚动,我们会看到库实现的功能和方法。这就是我们想要的细节:

使用 pow()函数

其中一个函数应该引起我们的注意,那就是pow(),或者幂函数。它返回第一个参数(double a)的值提高到第二个参数(double b)的幂。简而言之,它允许我们将数字提高到任意幂:

让我们回到编码。好的,让我们在声明变量number之后使用pow()函数来修改它的值。我们要做的事情是number = pow之类的事情,但我们需要更多的信息:

我们如何使用这个pow()函数?嗯,如果我们点击我们的文档,我们会看到当pow()函数被声明时,除了它的名称之外,还有在括号之间指定的两个参数。这些参数,double adouble b,是函数在操作之前请求的两个信息。

为了使用这个函数,我们的工作是用实际变量或显式值替换请求的double adouble b,以便pow()函数可以发挥作用。我们的文档告诉我们,double a应该被替换为我们想要提高到double b次幂的变量或值。

所以让我们用我们想要提高到任意幂的变量number替换第一个类型参数。在这一点上,numberfloat而不是double,除非我们简单地将其更改为double,否则这将给我们带来一些麻烦。所以让我们这样做。对于第二个参数,我们没有一个预先创建的变量来替换double b,所以让我们使用一个显式值,比如4.0

注意,当我调用pow()函数时,我去掉了double说明符。这个说明符只是为了让我们知道 Java 期望的类型。

理论上,pow()函数现在具有运行并将我们的数字变量的值提高到 4 次幂所需的所有信息。然而,NetBeans 仍然给我们显示红色警告标志。现在,这是因为 NetBeans,以及 Java 本身,不知道在哪里找到这个pow关键字。出于与我们需要指定完整路径到System.out.println()相同的原因,我们需要指定一个完整路径,以便 Java 可以找到pow()函数。这是我们在文档中找到pow()函数的路径。因此,让我们在我们的代码中指定java.lang.Math.pow()作为它的路径:

package themathlib; 

public class TheMathLib { 
    public static void main(String[] args) { 
        double number = 4.321; 
        number = java.lang.Math.pow(number, 4.0); 
        System.out.println(number); 
    } 
} 

现在我们基本上可以开始了。让我们在println语句中使用一次number变量,然后我们应该能够运行我们的程序:

如果我们想的话,我们可以将它插入我们的计算器,但我非常有信心,我们的程序已经输出了 4.321 的值提高到 4 次幂。

这很棒!我们刚刚使用外部代码不仅使我们的程序更容易编写,而且使它非常易读。它所需的代码行数比以前少得多。

导入类库

关于我们的程序,有一件事不太容易阅读,那就是到pow()println()等函数的长路径。我们能不能缩短它们?当然可以。如果 Java 的制造商想要的话,他们可以让我们在所有情况下通过简单地输入Math.pow()来调用这个函数。不幸的是,这可能会产生一些意想不到的副作用。例如,如果有两个库链接到 Java,并且它们都声明了Math.pow()函数,Java 将不知道使用哪一个。因此,默认情况下,我们期望直接和明确地链接到库。

因此,如果我们想要只输入Math.pow(),我们可以将一个库导入到我们正在工作的本地空间中。我们只需要在我们的类和main()函数声明上面执行一个import命令。导入命令所需的输入只是我们希望 Java 在遇到一个关键字时查找的路径,比如pow(),它不立即识别。为了让我们在程序中使用更简单的语法Math.pow(),我们只需要输入import java.lang.Math

package themathlib; 

import java.lang.Math; 

public class TheMathLib { 
    public static void main(String[] args) { 
        double number = 4.321; 
        number = java.lang.Math.pow(number, 4.0); 
        System.out.println(number); 
    } 
} 

有一些特殊的导入语法。假设我们想要导入java.lang中的所有类库。为了做到这一点,我们可以用.*替换.Math,并将其变为java.lang.*,这意味着“导入java.lang包中的每个库”。我应该告诉你,在 NetBeans 中工作的人,这个导入是默认完成的。然而,在这种情况下,我们将明确地这样做,因为你可能在其他 Java 环境中工作时也需要这样做。

字符变量

操作数字的程序都很好,但通常我们也想要能够处理文本和单词。为了帮助我们做到这一点,Java 定义了字符或char,原始类型。字符是您可以在计算机上处理的最小文本实体。一开始我们可以把它们想象成单个字母。

让我们创建一个新项目;我们将其命名为Characters.java。我们将通过简单地定义一个单个字符来开始我们的程序。我们将其称为character1,并将其赋值为大写的H

package characters; 

public class Characters { 
    public static void main(String[] args) { 
        char character1 = 'H'; 
    } 
} 

就像在明确定义浮点数时我们必须使用一些额外的语法一样,当定义一个字符时,我们需要一些额外的语法。为了告诉 Java 我们在这里明确声明一个字符值,我们用两个单引号将我们想要分配给变量的字母括起来。单引号与双引号相反,让 Java 知道我们正在处理一个字符或一个单个字母,而不是尝试使用整个字符串。字符只能有单个实体值。如果我们尝试将Hi的值分配给character1,NetBeans 和 Java 都会告诉我们这不是一个有效的选项:

现在,让我们继续编写一个有些复杂但对我们的示例目的非常有效的程序。让我们定义五个字符。我们将它们称为character1character5。我们将它们中的每一个分配给单词"Hello"的五个字母中的一个,按顺序。当这些字符一起打印时,我们的输出将显示Hello。在我们程序的第二部分,让我们使用System.out.print()在屏幕上显示这些字母。System.out.print()代码的工作方式与System.out.println()完全相同,只是它不会在我们的行末添加回车。让我们将最后一个命令设置为println,这样我们的输出就与控制台中呈现的所有附加文本分开了:

package characters; 

public class Characters { 

    public static void main(String[] args) { 
        char character1 = 'H'; 
        char character2 = 'e'; 
        char character3 = 'l'; 
        char character4 = 'l'; 
        char character5 = 'o'; 
        System.out.print(character1); 
        System.out.print(character2); 
        System.out.print(character3); 
        System.out.print(character4); 
        System.out.println(character5); 
    } 
} 

如果我们运行这个程序,它会向我们打招呼。它会说Hello,然后还会有一些额外的文本:

这很简单。

现在让我向您展示一些东西,这将让我们对计算机如何处理字符有一点了解。事实证明,我们不仅可以通过在两个单引号之间明确声明大写字母H来设置character1的值,还可以通过给它一个整数值来设置它的值。每个可能的字符值都有一个相应的数字,我们可以用它来代替。如果我们用值72替换H,我们仍然会打印出Hello。如果我们使用值73,比72大一的值,而不是大写字母H,我们现在会得到大写字母I,因为 I 是紧随 H 之后的字母。

我们必须确保不要在两个单引号之间放置72。最好的情况是 Java 会认识到72不是一个有效的字符,而更像是两个字符,那么我们的程序就不会编译。如果我们用单引号括起来的单个数字,我们的程序会编译得很好,但我们会得到完全意想不到的输出7ello

那么我们如何找出字符的数值呢?嗯,有一个通用的查找表,ASCII表,它将字符映射到它们的数值:

在本节中,我们一直在处理第 1 列(Dec)和第 5 列(Chr),它们分别是十进制数和它们映射到的字符。您会注意到,虽然许多这些字符是字母,但有些是键盘符号、数字和其他东西,比如制表符。就编程语言而言,换行、制表符和退格都是字符元素。

为了看到这个过程,让我们尝试用十进制值9替换程序中的一些字符,这应该对应一个制表符。如果我们用制表符替换单词中间的三个字母,作为输出,我们应该期望H,三个制表符和o

package characters; 

public class Characters { 

    public static void main(String[] args) { 
        char character1 = 'H'; 
        char character2 = 9; 
        char character3 = 9; 
        char character4 = 9; 
        char character5 = 'o'; 
        System.out.print(character1); 
        System.out.print(character2); 
        System.out.print(character3); 
        System.out.print(character4); 
        System.out.println(character5); 
    } 

以下是前述代码的输出:

字符串

让我们谈谈 Java 中的字符串。首先,创建一个新的 NetBeans 项目,命名为StringsInJava,并输入main()函数。然后,声明两个变量:一个名为c的字符和一个名为sString。很快,我们就清楚地看到String有点不同。您会注意到 NetBeans 没有选择用蓝色对我们的String关键字进行着色,就像我们声明原始类型的变量时那样:

这是因为String不像char那样是原始类型。String是我们所谓的类。类是面向对象编程的支柱。正如我们可以声明原始类型的变量一样,我们也可以声明类的变量,称为实例。在我们的程序中,变量sString类的一个实例。与原始类型的变量不同,类的实例可以包含由它们是实例的类声明的自己的特殊方法和函数。在本节中,我们将使用一些特定于字符串的方法和函数来操作文本。

但首先,让我们看看String类有什么特别之处。我们知道,我们几乎可以将字符变量和字符文字互换使用,就像我们可以用任何其他原始类型一样。String类也可以与字符串文字互换使用,它类似于字符文字,但使用双引号并且可以包含许多或没有字符。大多数 Java 类不能与任何类型的文字互换,而我们通过String类来操作字符串文字的能力正是它如此宝贵的原因。

连接运算符

字符串还有一项功能,大多数 Java 类都做不到,那就是利用加号(+)运算符。如果我们声明三个字符串(比如s1s2s3),我们可以将第三个字符串的值设置为一个字符串加上另一个字符串。我们甚至可以将一个字符串文字添加到其中。然后,我们打印s3

package stringsinjava; 

public class StringsInJava { 

    public static void main(String[] args) { 
        char c = 'c'; 
        String s1 = "stringone"; 
        String s2 = "stringtwo"; 
        String s3 = s1+s2+"LIT"; 
        System.out.println(s3); 
    } 
} 

当我们运行这个程序时,我们将看到这三个字符串被添加在一起,就像我们所期望的那样:

toUpperCase()函数

所以我向您承诺,字符串具有简单原始类型中看不到的功能。为了使用这个功能,让我们转到我们的 Java 文档中的String类,网址是docs.oracle.com/javase/7/docs/api/。在 Packages 下选择 java.lang,然后向下滚动并选择 ClassSummary 中的 String。与所有 Java 类的文档一样,String 文档包含 Method Summary,它将告诉我们关于现有String对象可以调用的所有函数。如果我们在 Method Summary 中向下滚动,我们将找到toUpperCase()函数,它将字符串中的所有字符转换为大写字母:

现在让我们使用这个函数。回到 NetBeans,我们现在需要确定在我们的程序中使用toUpperCase()函数的最佳位置:

package stringsinjava; 
public class StringsInJava { 
    public static void main(String[] args) { 
        char c = 'c'; 
        String s1 = "stringone"; 
        String s2 = "stringtwo"; 
        String s3 = s1 + s2 + "LIT"; 
        System.out.println(s3); 
    } 
} 

我们知道我们需要在StringsInJava.java程序中确定s3的值之后,使用toUpperCase()函数。我们可以做以下两件事中的任何一件:

  • 在确定s3的值之后,立即在下一行上使用该函数(只需键入s3.toUpperCase();)。

  • 在我们打印出s3的值的那一行的一部分中调用该函数。我们可以简单地打印出s3的值,也可以打印出s3.toUpperCase()的值,如下面的代码块所示:

package stringsinjava; 

public class StringsInJava { 

   public static void main(String[] args) { 
      char c = 'c'; 
      String s1 = "stringone"; 
      String s2 = "stringtwo"; 
      String s3 = s1+s2+"LIT"; 

      System.out.println(s3.toUpperCase()); 
   } 
} 

如果您还记得我们的文档,toUpperCase()函数不需要参数。它知道它是由s3调用的,这就是它所需要的所有知识,但我们仍然提供双空括号,以便 Java 知道我们实际上正在进行函数调用。如果我们现在运行这个程序,我们将得到预期的字符串大写版本:

但是,重要的是我们要理解这里发生了什么。System.out.println(s3.toUpperCase());代码行并不修改s3的值,然后打印出该值。相反,我们的println语句评估s3.toUpperCase(),然后打印出该函数返回的字符串。为了看到s3的实际值并没有被这个函数调用修改,我们可以再次打印s3的值:

System.out.println(s3.toUpperCase()); 
System.out.println(s3); 

我们可以看到s3保留了它的小写组件:

如果我们想永久修改s3的值,我们可以在上一行这样做,并且我们可以将s3的值设置为函数的结果:

package stringsinjava; 

public class StringsInJava { 
    public static void main(String[] args) { 
        char c = 'c'; 
        String s1 = "stringone"; 
        String s2 = "stringtwo"; 
        String s3 = s1 + s2 + "LIT"; 

        s3 = s3.toUpperCase(); 

        System.out.println(s3); 
        System.out.println(s3); 
    } 
} 

以下是前面代码的输出:

replace()函数

为了确认我们都在同一页面上,让我们再使用String类的一个方法。如果我们回到我们的文档并向上滚动,我们可以找到 String 的replace()方法:

与我们的toUpperCase()方法不同,它不带参数,replace()需要两个字符作为参数。该函数将返回一个新的字符串,其中我们作为参数给出的第一个字符(oldChar)的所有实例都被我们作为参数给出的第二个字符(newChar)替换。

让我们在StringsInJava.java的第一个println()行上使用这个函数。我们将输入s3.replace()并给我们的函数两个字符作为参数。让我们用字符g替换字符o

package stringsinjava; 

public class StringsInJava { 
    public static void main(String[] args) { 
       char c = 'c'; 
        String s1 = "stringone"; 
        String s2 = "stringtwo"; 
        String s3 = s1 + s2 + "LIT"; 

        s3 = s3.toUpperCase(); 

        System.out.println(s3.replace('g', 'o')); 
        System.out.println(s3); 
    } 
} 

当我们运行我们的程序时,当然什么也不会发生。这是因为当我们到达打印语句时,没有小写的g字符,也没有剩余的小写的g字符在s3中;只有大写的G字符。所以让我们尝试替换大写的G字符:

System.out.println(s3.replace('G', 'o')); 
System.out.println(s3); 

现在,如果我们运行我们的程序,我们会看到替换发生在第一个println的实例上,而不是第二个实例上。这是因为我们实际上没有改变s3的值:

太好了!现在你已经装备精良,只要你随时准备好 Java 文档,就可以调用各种String方法。

转义序列

如果你花了很多时间处理字符串,我预计你会遇到一个常见的问题。让我们快速看一下。我要在这里写一个全新的程序。我要声明一个字符串,然后让我们的程序将字符串打印到屏幕上。但我要给这个字符串赋值的值会有点棘手。我希望我们的程序打印出The program says: "Hello World"(我希望Hello World被双引号括起来):

这里的问题是,在字符串文字中放置双引号会让 Java 感到困惑,就像前面的屏幕截图所示的那样。当它阅读我们的程序时,它看到的第一个完整字符串是"The program says:",这告诉 Java 我们已经结束了字符串。这当然不是我们想要的。

幸运的是,我们有一个系统可以告诉 Java,我们希望一个字符被视为字符文字,而不是它可能具有的特殊功能。为此,我们在字符前面放一个反斜杠。这被称为转义序列:

String s= "The program says: \"Hello World\""; 
System.out.println(s); 

现在,当 Java 阅读这个字符串时,它将读取The program says:,然后看到反斜杠,并知道如何将我们的双引号视为双引号字符,而不是围绕字符串的双引号。当我们运行我们的程序时,我们将看不到反斜杠;它们本身是特殊字符:

如果我们确实想在字符串中看到反斜杠,我们需要在其前面加上一个反斜杠:

String s= "The program says: \\ \"Hello World\""; 
System.out.println(s); 

这就是字符串 101!

总结

在本章中,我们解释了变量是什么,以及它们对于创建更好的程序有多重要。我们详细介绍了 Java 的一些原始数据类型,即intlongfloatchardouble。我们还看到了String类及其两种操作方法。

在下一章中,我们将看一下 Java 中的分支语句。

第三章:分支

每次运行时执行相同操作的程序都很好,但最有趣的计算机程序每次运行时都会做一些不同的事情,这可能是因为它们具有不同的输入,甚至是因为用户正在积极地与它们交互。有了这个,让我们通过理解条件语句来启动本章,然后我们将进一步探讨 Java 如何处理复杂的条件语句,修改程序的控制流,并研究循环功能。

具体来说,本章将涵盖以下主题:

  • 理解if语句

  • 复杂的条件语句

  • switchcasebreak语句

  • whiledo...while循环

  • for循环

理解 if 语句

今天,我们将探讨非常基本的ifelse条件语句。要进一步理解这一点,请参考以下项目列表:

  1. 让我们在 NetBeans 中创建一个新的 Java 项目。我将把我的项目命名为ConditionalStatements,并允许 NetBeans 为我创建main类;参考以下截图:

为了保持清晰,我们可以摆脱所有的注释;现在我们可以开始了。为了让我们编写更有趣的程序,我们将快速学习如何在 Java 中进行一些基本的用户输入。在这个时候,你还没有足够的知识基础来完全理解我们即将要做的复杂性,但是你可能对正在发生的事情有基本的理解,并且将来肯定可以自己重复这个过程。

在这个InputStream/Console窗口中写入是一种简单的一次性过程,但是在 Java 中读取输入可能会更加复杂:

  1. 用户输入被放入一个缓冲区,我们的程序在提示时访问它;因此,我们需要声明一个变量,允许我们在需要获取新用户输入时访问这个缓冲区。为此,我们将使用Scanner类。让我们称我们的新实例为reader。NetBeans 对我们大喊大叫,因为Scanner位于java.util包中,我们需要显式访问它。我们可以随时导入java.util包:
package conditionalstatements; 

public class ConditionalStatements { 

    public static void main(String[] args) { 
        java.util.Scanner reader; 
    } 
} 
  1. 这是你需要有点信心并超前一点,超出你现在真正准备完全理解的范围。我们需要为reader变量分配一个值,这个值是Scanner类型的,这样它就可以连接到 InputStream 窗口,用户将在其中输入。为此,我们将把它的值设置为一个全新的Scanner()对象的值,但是这个 Scanner 对象将使用一个类型参数,即(System.in),这恰好是我们的用户将要使用的 InputStream 的链接:
package conditionalstatements; 

import java.util.*; 

public class ConditionalStatements { 

    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
    } 
} 

以下是前面代码的输出:

  1. 就像我说的,这是一些重要的内容,你肯定不应该期望现在就完全理解它是如何工作的。现在,知道reader与我们的 InputStream 窗口连接,我们的Scanner对象具有next()函数,允许我们访问用户刚刚输入到流中的输入。就像大多数函数一样,这个函数只是返回这个输入,所以我们需要创建一个字符串来存储这个输入。

  2. 完成这些后,我们可以使用System.out.println()函数将input值打印回控制台:

package conditionalstatements; 

import java.util.*; 

public class ConditionalStatements { 

    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        String input = reader.next(); 
        System.out.println(input); 
    } 
} 
  1. 当我们运行程序时,似乎没有任何事情发生,但实际上,我们的控制台在这里等待用户输入。现在,当我们在控制台中输入我们的输入并按下Enter键时,它将立即回显给我们:

  2. 我们可以通过让程序提示用户输入而不是静静地等待来使其更加友好:

public static void main(String[] args) { 
    Scanner reader = new Scanner(System.in); 
    System.out.println("Input now: "); 
    String input = reader.next(); 
    System.out.println(input); 
} 

条件语句

在本章的开头,我承诺过你会学习条件语句,我们现在就要做到这一点。但首先,让我们对我们程序的用户输入部分进行一个小修改。与其获取一个字符串,如果我们学习使用用户提供的整数值来工作,那将会更容易得多。因此,让我们将我们的input变量的值或类型更改为int数据类型;reader.next()函数返回一个字符串,但有一个类似的函数叫做nextInt(),它将返回一个整数:

int input = reader.nextInt(); 

我们肯定不会在我们非常简单的程序中加入任何错误处理机制。

要知道,如果我们不小心向这个 Java 程序提供除整数以外的任何东西,程序将崩溃。

那么条件语句到底是什么?条件语句允许我们根据某些事情是真还是假,将我们的程序引导到不同的路径上,执行不同的代码行。在本章中,我们将使用条件语句根据用户给我们的输入值来打印不同的响应。具体来说,我们将告诉他们他们给我们的值是小于、大于还是等于数字 10。为了开始这个过程,让我们设置我们的输出情况。

如果我们的用户提供的输入大于 10,我们打印出MORE。如果用户提供的输入恰好小于 10,我们打印出LESS。当然,如果我们现在运行这个程序,它将简单地打印出MORELESS,两行都会打印。我们需要使用条件语句来确保这两行中只有一行在任何程序运行中执行,并且当然执行正确的行。您可能已经注意到,NetBeans 为我们创建的默认项目将我们的代码分成了用大括号括起来的段。

我们可以使用自己的括号进一步将我们的代码分成段。惯例规定,一旦我们创建了一组新的括号,一个新的代码段,我们需要在括号之间的所有内容之前添加一个制表符,以使我们的程序更易读。

使用 if 语句

一旦我们将我们的两个system.out.println语句分开,我们现在可以提供必须为真的情况,如果这些语句要运行的话。为此,我们用 Java 的if语句作为前缀,其中if是一个 Java 关键字,后面跟着两个括号,我们在括号之间放置要评估的语句。如果 Java 确定我们在括号之间写的语句为真,则以下括号中的代码将执行。如果 Java 确定该语句为假,则括号中的代码将被完全跳过。基本上,我们将给这个if语句两个输入。我们将给它变量input,如果你还记得,它包含我们从用户那里得到的整数值,我们将给它显式值10,这是我们要比较的值。Java 理解大于(>)和小于(<)比较运算符。因此,如果我们使这个if语句if(input > 10),那么System.out.println命令(如下面的屏幕截图中所示)只有在用户提供大于 10 的值时才会运行:

if(input > 10) 
        { 
            System.out.println("MORE!"); 
        } 
        { 
            System.out.println("LESS!"); 
        } 

现在,我们需要提供一个if语句,以确保我们的程序不会总是打印出LESS

我们可以使用小于运算符,要求我们的程序在用户提供小于 10 的输入时打印出LESS。在几乎所有情况下,这都是很好的,但如果我们的用户提供的输入值是 10,我们的程序将什么也不打印。为了解决这个问题,我们可以使用小于或等于运算符来确保我们的程序始终对用户输入做出响应:

package conditionalstatements; 

import java.util.*; 

public class ConditionalStatements { 

    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        System.out.println("Input now: "); 
        int input =  reader.nextInt(); 

        if(input > 10) 
        { 
            System.out.println("MORE!"); 
        } 
        if(input <= 10) 
        { 
            System.out.println("LESS"); 
        } 
    } 
} 

现在,让我们快速运行我们的程序,确保它能正常工作。

在 InputStream 窗口中有一个输入提示。让我们首先给它一个大于 10 的值,然后按Enter键。我们得到了MORE的响应,而不是LESS的响应;这是我们预期的结果:

我们的程序不循环,所以我们需要再次运行它来测试LESS输出,这次让我们给它一个值10,这应该触发我们的小于或等于运算符。大功告成!

使用 else 语句

事实证明,有一种稍微更容易的方法来编写前面的程序。当我们编写一个条件语句或者说一对条件语句,其中我们总是要执行两个代码块中的一个时,现在可能是使用else关键字的好时机。else关键字必须跟在带括号的if块后面,然后跟着它自己的括号。else语句将在前一个if括号之间的代码未执行时评估为 true,并执行其括号之间的代码:

import java.util.*; 

public class ConditionalStatements { 

    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        System.out.println("Input now: "); 
        int input =  reader.nextInt(); 

        if(input > 10) 
        { 
            System.out.println("MORE!"); 
        } 
       else 
        { 
            System.out.println("LESS"); 
        } 
    } 
} 

如果我们运行这个程序,我们将得到与之前相同的结果,只是少写了一点逻辑代码:

让我们以简要介绍我们的if语句中可以使用的其他运算符结束这个话题,然后我们将看看如果需要比较非原始类型的项目该怎么办。除了大于和小于运算符之外,我们还可以使用相等运算符(==),如果两侧的项目具有相同的值,则为 true。当使用相等运算符时,请确保不要意外使用赋值运算符(=):

if(input == 10) 

在某些情况下,您的程序不会编译,但在其他情况下,它将编译,并且您将得到非常奇怪的结果。如果您想使用相等运算符的相反操作,可以使用不等于(!=),如果两个项目的值不相同,则返回 true:

if(input != 10) 

重要的是,当比较类的实例时,我们不应尝试使用这些相等运算符。我们只应在处理原始类型时使用它们。

为了证明这一点,让我们修改我们的程序,以便我们可以将String作为用户输入。我们将看看String是否等同于秘密密码代码:

如果是,它将打印出YES;如果不是,它将打印出NO。现在,NetBeans 给了我们一个警告(如前面的截图所示);实际上,如果我们尝试使用一些不同的运算符来比较字符串,NetBeans 会让我们知道我们的程序可能甚至无法编译。这是因为 Java 不希望我们使用这些运算符来比较类的实例。相反,类应该公开允许我们逻辑比较它们的函数。几乎每个 Java 对象都有一些用于此目的的函数。其中最常见的之一是equals()函数,它接受相同类型的对象,并让我们知道它们是否等价。这个函数返回一个称为布尔类型的东西,它是自己的原始类型,可以具有 true 或 false 的值。我们的if语句知道如何评估这个布尔类型:

import java.util.*; 

public class ConditionalStatements { 

    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        System.out.println("Input now: "); 
        String input =  reader.next(); 

        if(input.equals("password")) 
        { 
            System.out.println("YES"); 
        } 
        else 
        { 
            System.out.println("NO"); 
        } 
    } 
} 

让我们快速运行我们的程序,首先输入一个错误的字符串,然后输入password来看看我们的程序是否工作:

这就是if-else语句的基础。我现在鼓励你尝试一些我们看过的比较运算符,并尝试在彼此之间嵌套if...else语句。

最后一点,有时您可能会看到没有后续括号的if语句。这是有效的语法,基本上相当于将整个语句放在一行上。

复杂条件

首先,让我们编写一个非常简单的 Java 程序。我们将首先导入java.util,以便我们可以通过Scanner对象获得一些用户输入,并将这个Scanner对象与System.in输入字符串链接起来,这样我们就可以在控制台窗口中使用它。

完成这些后,我们需要从用户那里获取一些输入并存储它,因此让我们创建一个新的字符串并将其值分配给用户给我们的任何值。为了保持事情有趣,让我们给自己再增加两个 String 变量来使用。我们将它们称为sOnesTwo;我们将第一个字符串变量的值分配为abc,第二个字符串变量的值分配为z

package complexconditionals; 

import java.util.*; 

public class ComplexConditionals { 
    public static void main(String[] args) { 
      Scanner reader = new Scanner (System.in); 
      String input = reader.next(); 
      String sOne = "abc"; 
      String sTwo = "z"; 
    } 
} 

因为这个话题是关于条件语句,我们可能需要其中之一,所以让我们创建一个if...else块。这是我们将评估我们条件语句的地方。我们将设置一些输出,这样我们就可以看到发生了什么。如果我们的条件语句评估为 true 并且我们进入块的以下部分,我们将简单地打印出TRUE

if() 
{ 
    System.out.println("TRUE");     
} 
else 
{ 

} 

如果条件语句评估为 false 并且我们跳过块的前一个if部分,而是进入else部分,我们将打印出FALSE

if() 
{ 
    System.out.println("TRUE");     
} 
else 
{ 
    System.out.println("FALSE"); 
} 

包含函数

现在可能是时候编写我们的条件语句了。让我向您介绍一个名为contains函数的新字符串函数:

if(input.contains()) 

contains函数接受一个字符序列作为输入,其中包含一个字符串的资格。作为输出,它给我们一个布尔值,这意味着它将输出TRUEFALSE。因此,我们的if语句应该理解这个函数的结果并评估为相同。为了测试我们的程序,让我们首先简单地通过以下过程。

我们将为我们的contains函数提供存储在sOne字符串中的值,即abc

package complexconditionals; 

import java.util.*; 

public class ComplexConditionals { 
    public static void main(String[] args) { 
      Scanner reader = new Scanner (System.in); 
      String input = reader.next(); 
      String sOne = "abc"; 
      String sTwo = "z"; 
      if(input.contains(sOne)) 
      { 
           System.out.println("TRUE");     
      } 
      else 
      { 
           System.out.println("FALSE"); 
      } 
    } 
} 

因此,如果我们运行我们的程序并为其提供abcdefg,其中包含abc字符串,我们将得到TRUE的结果。这是因为input.contains评估为 true,我们进入了我们的if...else块的if部分:

如果我们运行并提供一些不包含abc字符串的胡言乱语,我们可以进入块的else语句并返回FALSE

没有太疯狂的地方。但是,假设我们想让我们的程序变得更加复杂。让我们在下一节中看看这个。

复杂的条件语句

如果我们想要检查并查看我们的输入字符串是否同时包含sOnesTwo两个字符串呢?有几种方法可以做到这一点,我们将看看一些其他方法。但是,对于我们的目的来说,可能最简单的方法是在if(input.contains(sOne))行上使用复杂条件。Java 允许我们使用&&|条件运算符一次评估多个 true 或 false 语句,或布尔对象。当与&&运算符比较的所有条件都评估为 true 时,&&运算符给我们一个 true 结果。当与|运算符比较的任何条件评估为 true 时,|运算符给我们一个 true 结果。在我们的情况下,我们想知道我们的输入字符串是否同时包含sOnesTwo的内容,所以我们将使用&&运算符。这个运算符通过简单地在它的两侧提供两个条件语句来工作。因此,我们将在sOnesTwo上运行我们的input.contains函数。如果&&运算符的两侧的这些函数都评估为 true,即(if(input.contains(sOne) && input.contains(sTwo)),我们的条件语句也将为 true:

package complexconditionals; 

import java.util.*; 

public class ComplexConditionals { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner (System.in); 
        String input = reader.next(); 
        String sOne = "abc"; 
        String sTwo = "z"; 
        if(input.contains(sOne)) 
        { 
            System.out.println("TRUE"); 
        } 
        else 
        { 
            System.out.println("FALSE"); 
        } 
    } 
} 

让我们运行我们的程序。abcz字符串在两种情况下都应该评估为 true,当我们按下Enter键时,我们看到实际情况确实如此:

如果我们只提供有效的字符串z,我们会得到一个 false 的结果,因为我们的&&运算符会评估为 false 和 true,这评估为 false。如果我们使用|运算符,这将是字符串:

if(input.contains(sOne) || input.contains(sTwo)) 

以下是前面代码的输出:

这实际上会给我们一个真正的结果,因为现在我们只需要其中一个函数返回 true。布尔逻辑可能会变得非常疯狂。例如,我们可以将&& false语句放在我们的布尔条件的末尾,即if(input.contains(sOne) || input.contains(sTwo) && false)。在 Java 中,truefalse代码术语是关键字;实际上,它们是显式值,就像数字或单个字符一样。true关键字评估为 true,false关键字评估为 false。

任何以false结尾的单个条件语句将始终作为整体评估为 false:

if(input.contains(sOne) && false) 

有趣的是,如果我们返回到我们之前的原始语句,并运行以下程序,提供它最有效的可能输入,我们将得到真正的结果:

package complexconditionals;
import java.util.*;
public class ComplexConditionals {
    public static void main(String[] args) {
        Scanner reader = new Scanner(System.in);
        String input = reader.next();
        String sOne = "abc";
        String sTwo = "z";
        if(input.contains(sOne) || input.contains(sTwo) && false)
        {
            System.out.println("TRUE");
        }
        else
        {
            System.out.println("FALSE");
        }
    }
}

以下是前面代码的输出:

有趣的是,如果 Java 首先评估if(input.contains(sOne) || input.contains(sTwo))语句,然后是&& false语句,我们将得到一个 false 的结果;相反,Java 似乎选择首先评估(input.contains(sTwo) && false)语句,然后是||语句,即(input.contains(sOne) ||)。这可能会让事情变得非常混乱。

幸运的是,就像在代数中一样,我们可以要求 Java 按特定顺序执行操作。我们通过用括号括起我们的代码块来做到这一点。括号内的代码块将在 Java 离开括号以评估其他内容之前进行评估:

if((input.contains(sOne) || input.contains(sTwo)) && false) 

因此,在我们用括号括起||语句之后,我们将计算||语句,然后以false结束该结果:

package complexconditionals;
import java.util.*;
public class ComplexConditionals {
    public static void main(String[] args) {
        Scanner reader = new Scanner(System.in);
        String input = reader.next();
        String sOne = "abc";
        String sTwo = "z";
        if((input.contains(sOne) || input.contains(sTwo)) && false)
        {
            System.out.println("TRUE");
        }
        else
        {
            System.out.println("FALSE");
        }
    }
}

我们现在将看到我们前面的程序总是在这里评估为 false:

复杂的条件可能会变得非常复杂。如果我们在代码中遇到这样的if语句,特别是如果这是我们没有编写的代码,可能需要花费很长时间才能弄清楚到底发生了什么。

布尔变量

为了帮助我们理解前面部分讨论的内容,我们有布尔变量:

boolean bool = true; 

在上一行代码中,boolean是 Java 中的一个原始类型,boolean类型的变量只能有两个值之一:它可以是truefalse。我们可以将我们的布尔变量的值设置为任何条件语句。因此,如果我们想要简化实际if语句中的代码外观,我们可以继续存储这些布尔值:

boolean bool1 = input.contains(sOne); 
boolean bool2 = input.contains(sTwo);  

在实际评估if语句之前,我们需要这样做,使一切更加紧凑和可读:

if((bool1 || bool2) && false) 

记住,游戏的名字是尽可能保持我们的代码简单和可读。一个非常长的条件语句可能写起来感觉很棒,但通常有更加优雅的解决方案。

这就是 Java 中复杂条件的实质。

Switch,case 和 break

在本节中,我们将看一下switch语句,这是我们可以修改程序控制流的另一种方式。

首先,在 NetBeans 中创建一个新项目。至少在我的端上,我要摆脱所有这些注释。为了展示switch语句的强大,我们将首先编写一个仅使用if块的程序,然后将程序转换为使用switch语句的程序。以下是仅使用if块的程序的步骤:

  1. 首先,让我们简单地声明一个变量xint x =1;),这是我们的目标:如果x的值是123,我们想要分别打印出响应REDBLUEGREEN。如果x不是这些数字之一,我们将只打印出默认响应。

  2. 使用if块做这件事情相当简单,尽管有点乏味:

if(x == 1) 
{ 
System.out.println("RED") 
} 

然后,我们基本上只需复制并粘贴这段代码,并为蓝色和绿色情况进行修改:

int x=1; 
if(x==1) 
{ 
    System.out.println("RED"); 
} 
if(x==2) 
{ 
    System.out.println("BLUE"); 
} 
if(x==3) 
{ 
    System.out.println("GREEN"); 
} 
  1. 对于我们的默认情况,我们只想检查x不等于1x不等于2x不等于3
if((x != 1) && (x != 2) && (x != 3)) 
{ 
    System.out.println("NONE"); 
} 

让我们快速运行一下我们的程序:

package switcher; 

public class Switcher { 
    public static void main(String[] args) { 
        int x=1; 

       if(x==1) 
       { 
           System.out.println("RED"); 
       } 
       if(x==2) 
       { 
           System.out.println("BLUE"); 
       } 
       if(x==3) 
       { 
           System.out.println("GREEN"); 
       } 
    } 
} 

以下是预期结果的屏幕截图:

这是我们在编写更大程序的过程中可能会做的事情的简化版本。虽然我们以相当快的速度组织了这个程序,但很容易看出,如果我们要处理许多可能的x情况,这个问题将变得非常难以控制。而且,对于某人来阅读和弄清楚这里发生了什么,也是相当困难的。解决方案,你可能已经猜到了,是使用switch语句来控制程序的流程。

使用 switch、case 和 break 的程序

当我们想要根据一个变量的值执行不同的行或代码块时,switch语句非常有效。现在让我们使用switch语句来重写我们的一系列if块。语法在以下步骤中解释:

  1. 我们首先声明我们将使用switch语句,switch是 Java 中的一个保留关键字。然后,我们提供我们希望switch语句作用的变量的名称,在这种情况下是x,因为我们将根据x的值执行不同的代码块:
package switcher; 

public class Switcher { 
    public static void main(String[] args) { 
        int x=1; 

        switch(x) 
        { 

        } 
    } 
} 

然后,就像使用ifelse语句一样,我们将使用两个括号创建一个新的代码段。

  1. 现在,我们不再创建一系列难以控制的if块,而是使用case关键字在我们的switch语句中创建单独的代码块。在每个case关键字之后,我们给出一个规定的值,如果x的值与case关键字的值匹配,接下来的代码将执行。

因此,就像我们在做if块时一样,如果x的值是1,我们想要打印出RED。现在为每种可能的值编写单独的情况变得更加清晰和易于阅读。

  1. switch语句还有一个特殊情况,即default情况,我们几乎总是将其放在switch语句的末尾。

只有在其他情况都没有执行时,这种情况才会执行,这意味着我们不必为我们最后的if块编写复杂的布尔逻辑:

package switcher; 

public class Switcher { 
    public static void main(String[] args) { 
        int x=7; 

        switch(x) 
        { 
            case 1: case 5: case 7: 
                System.out.println("RED"); 
            case 2: 
                System.out.println("BLUE"); 
            case 3: 
                System.out.println("GREEN"); 
            default: 
                System.out.println("NONE"); 
        } 
    }  
} 

如果我们运行前面的程序,实际上会看到每种可能的输出都会执行。这是因为我们忘记了做一件非常重要的事情:

switch语句允许我们创建复杂的逻辑树,因为一旦一个case开始执行,它将继续执行,即使通过了队列中的下一个case。因为我们正在编写一个非常简单的程序,我们只希望执行一个case,所以我们需要在进入一个case并完成代码后明确结束执行。

  1. 我们可以使用break关键字来做到这一点,它存在于一行代码中,并且简单地将我们从当前的case中跳出来:
package switcher; 

public class Switcher { 
    public static void main(String[] args) { 
        int x=1; 

        switch(x) 
        { 
            case 1: 
                System.out.println("RED"); 
                break; 
            case 2: 
                System.out.println("BLUE"); 
                break; 
            case 3: 
                System.out.println("GREEN"); 
                break; 
            default: 
                System.out.println("NONE"); 
        } 
    } 
} 

现在,如果我们运行我们的程序,我们将看到预期的结果:

  1. 除了从一个情况自由地转到另一个情况,我们还可以通过在一行中添加多个情况来增加我们的 switch 语句的复杂性和功能。因为情况自由地相互转到,做一些像case 1: case 5: case;这样的事情意味着如果我们提供这些数字之一:157,接下来的代码块将执行。所以这是switch语句的快速简单方法:
package switcher; 

public class Switcher { 
    public static void main(String[] args) { 
        int x=7; 

        switch(x) 
        { 
            case 1: case 5: case 7: 
                System.out.println("RED"); 
                break; 
            case 2: 
                System.out.println("BLUE"); 
                break; 
            case 3: 
                System.out.println("GREEN"); 
                break; 
            default: 
                System.out.println("NONE"); 
        } 
    } 
} 

以下是前面代码的输出:

Switch 语句基本上是使用等号(==)运算符比较我们正在切换的变量或显式值和情况。如果元素不能使用等号运算符进行比较,switch 语句将无法正常工作。

从 Java SE v7 开始,您可以使用等号运算符比较字符串,因此可以在switch语句中使用它们。这并不总是这样,而且最好避免在switch语句中使用等号运算符与字符串。这是因为它破坏了您正在编写的代码的向后兼容性。

While 和 do...while 循环

欢迎来到循环的入门课程。在本节结束时,我们将掌握 Java 的whiledo...while循环。我对此感到非常兴奋,因为循环允许我们执行一块 Java 代码多次,正如我们所希望的那样。这是我们学习过程中非常酷的一步,因为能够连续多次执行小任务是使计算机在某些任务上比人类更优越的原因之一:

  1. 开始这个话题,让我们创建一个新的 NetBeans 项目,输入main方法,然后简单地声明一个整数并给它一个值。我们可以选择任何正值。我们将要求我们的程序打印出短语Hello World的次数等于我们整数的值。

  2. 为此,我们将使用while循环。while循环的语法看起来很像我们在写一个if语句。我们从保留的while关键字开始,然后跟着两个括号;在这些括号里,我们最终会放置一个条件语句。就像它是一个if语句一样,只有当我们的程序到达我们的while循环并且评估其条件语句为真时,接下来的代码块才会执行:

package introtoloops; 

public class IntroToLoops { 
    public static void main(String[] args) { 
        int i=5; 

        while () 
        { 

        }  
    } 
} 

然而,将while循环与if语句分开的是,当到达while循环的代码块的末尾时,我们的程序基本上会跳回并再次执行这行代码,评估条件语句并且如果条件语句仍然为真,则重新进入while循环的代码块。

让我们从设置while循环的逻辑开始。我们希望循环执行的次数存储在整数 i 的值中,但我们需要一种方法将这个值传达给我们的循环。嗯,任何不会无限运行的循环都需要在循环内容中进行一些控制流的改变。在我们的情况下,让我们每次循环运行时改变程序的状态,通过减少 i 的值,这样当 i 达到 0 时,我们将循环运行了五次。

  1. 如果是这种情况,这意味着我们只希望我们的循环在i的值大于0时执行。让我们暂停一下,快速看一下这行代码。这里i = i -1是一个完全有效的语句,但我们可以使用一个更快更容易阅读的快捷方式。我们可以使用i--来将整数变量的值减少一。一旦我们设置好这个,唯一剩下的事情就是将功能代码放在我们的循环内;那就是一个简单的println语句,说Hello world
package introtoloops; 

public class IntroToLoops { 
    public static void main(String[] args) { 
        int i=5; 

        while (i>0) 
        { 
            System.out.println("Hello world"); 
            i--; 
        }  
    } 
} 
  1. 现在,让我们运行我们的程序,看看会发生什么:

好了,五个Hello world实例打印到我们的控制台窗口中,就像我们打算的那样。

While 循环

通常,我们允许小程序,比如我们在这里编写的程序,在没有更多代码可执行时结束。但是,在使用循环时,我们可能会错误地创建一个无限的while循环并运行一个没有结束的程序:

package introtoloops; 

public class IntroToLoops { 
    public static void main(String[] args) { 
        int i=5; 

        while (i>0) 
        { 
            System.out.println("Hello world"); 
        }  
    } 
} 

当这种情况发生时,我们需要手动关闭我们的程序。在 NetBeans 中,输出窗口的左侧有一个称为“停止”的方便小功能:

如果我们通过命令提示符运行程序,“Ctrl”+“C”是取消执行程序的常用命令。现在我们已经掌握了基本的while循环语法,让我们尝试一些更复杂和更动态的东西:

  1. 我心目中的程序将需要一些用户输入,因此让我们导入java.util并设置一个新的Scanner对象:
public class IntroToLoops { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
  1. 不过,我们不会立即收集用户输入,而是每次我们的while循环成功执行时收集新的用户输入:
while(i > 0) { 
   reader.nextLine(); 
   System.out.println("Hello world"); 
} 
  1. 每次我们收集这个输入,我们都需要一个地方来存储它,所以让我们创建一个新的字符串,其目的是存储新获取的输入的值:
public class IntroToloops { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        String input; 
        int i=5; 
        while(i>0) { 
            input= reader.nextLine(); 
            System.out.println("Hello world"); 
        } 
    }   
} 

这个input变量的值将在程序执行过程中多次更改,因为在每个while循环的开始,我们将为它分配一个新值。如果我们简单地执行这个程序,对我们用户来说将不会很有趣,因为当我们为它分配一个新值时,输入字符串的旧值将不断丢失。

  1. 因此,让我们创建另一个字符串,其目的是存储我们从用户那里得到的所有连接值。然后,在我们的程序结束时,我们将打印出这个字符串的值,以便用户可以看到我们一直在存储他们的输入:
public class IntroToloops { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        String input; 
        String all = ""; 
        int i=5; 
        while(i>0) { 
            input = reader.nextLine(); 
        }   
        System.out.println(all); 
    }   
}
  1. 在这里所示的行上将输入的值添加到所有字符串中:
while(i>0) { 
   input = reader.nextLine(); 
   all = 
}

我们可以做一些事情。我们可以使用加法运算符很好地添加字符串。因此,all = all + input语句,其中allinput是字符串,加号是完全有效的。但是,当我们将某物添加到它自身并使用原始类型或可以像字符串一样起作用的类型时,我们还可以使用+=运算符,它执行相同的功能。此外,我们不能忘记重新实现整数值i的递减,以便我们的程序不会无限运行:

package introtoloops; 
import java.util.*; 
public class IntroToLoops { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 

        String input; 
        String all = ""; 
        int i=5; 

        while (i>0) { 
            input = reader.nextLine(); 
            all += input; 
            i--; 
        }  
        System.out.println(all); 
    } 
 }  

现在,如果我们运行这个程序并提供五个输入字符串,我们将得到如下屏幕截图所示的输出:

我们将看到它们如预期般全部输出,这很酷,但我对这个程序有更大的计划。

实际上,如果我们只想编写我们在这里拥有的程序,稍后我们将学习的for循环可能完全合适。但是对于我们即将要做的事情,whiledo...while循环是非常必要的。我想做的是在这个程序中摆脱我们的计数变量。相反,我们将允许用户告诉我们何时停止执行我们的程序。

当用户将输入的值设置为STOP字符串时,以所有大写字母,我们将退出执行我们的while循环并打印出他们迄今为止给我们的所有字符串。因此,我们只希望这个while循环在输入的值不是STOP值时运行。您会注意到,我们将得到一个预编译错误,如下屏幕截图所示:

如果我们尝试运行程序,我们将会得到一个完整的编译器错误。这是因为我们的程序知道,当我们尝试执行这个条件语句的时候,输入的值还没有被设置。即使输入的不存在的值不等于STOP,这也是非常糟糕的形式。在这里的字符串情况下,它不是一个原始的值,我们的计算机在给它任何值之前是不可能访问它的任何方法的。

这里一个不太优雅的解决方案是给输入一个起始值,就像我们在all中所做的那样,但有一个更好的方法。一旦我们的循环执行了一次,我们知道输入将会有一个由用户给出的正确值,这个值可能是STOP,也可能不是。

do...while 循环

如果我们不是在循环的开始检查条件,而是在循环的结束检查条件呢?这实际上是一个选项。do...while循环的操作方式与while循环相同,但第一次运行时,它们不会检查条件是否为真;它们只会运行并在最后检查它们的条件语句。我们需要在do...while循环的后面的条件语句的末尾加上一个分号。我提到这个是因为我总是忘记。现在,如果我们运行我们的程序,我们可以输入任意数量的字符串,然后输入STOP字符串,以查看到目前为止我们输入的所有内容并打印到屏幕上:

public static void main(String[] args) { 
    Scanner reader = new Scanner(System.in); 

    String input; 
    String all = ""; 
    int i=5; 

    do 
    { 
        input = reader.nextLine(); 
        all += input; 
        i--; 
    } while(!input.equals("STOP")); 
    System.out.println(all); 
} 

以下是前面代码的输出:

最后一点说明,几乎任何后面跟着自己代码块的东西,你会看到这样的语法,你会有一个关键字,可能是一个条件语句,然后是后面的括号;或者,你可能会看到括号从与关键字和条件语句相同的行开始。这两种方法都是完全有效的,事实上,括号从与关键字相同的行开始可能很快就变得更加普遍。

我鼓励你玩弄一下我们写的程序。尝试执行你认为会推动字符串所能容纳的信息量边界的循环,或者玩弄一下向屏幕呈现大量信息的循环。这是计算机做的事情,我们简单地无法用铅笔和纸做到,所以这很酷。

for 循环

在这一部分,我们将快速看一下for循环。我们使用for循环以非常语义优雅的方式解决 Java 中的一个常见问题。当我们需要迭代一个变量来计算我们循环了多少次时,这些循环是合适的。

首先,我写了一个非常基本的程序,使用了一个while循环;它将值1100打印到我们屏幕上的窗口。一旦你在脑海中理清了这个while循环是如何工作的,我们将使用for循环编写相同的循环,这样我们就可以看到在这种特定情况下for循环更加优雅。让我们注释掉我们的while循环,这样我们仍然可以在下面的截图中看到它,而不执行任何代码,并开始编写我们的for循环:

for循环的基本语法看起来非常类似于while循环。我们有保留关键字,在两个括号中我们将放一些循环需要的信息,以及我们将要循环的代码块。与while循环不同的是,while循环只在这些括号之间提供一个条件语句,而我们将为for循环提供大量信息。因为for循环设计用于处理特定情况,一旦我们提供了所有这些信息,它就会准确知道如何处理。这减轻了我们处理循环外的代码和在循环内手动递增或递减的需要。它使我们的代码的功能部分,即println语句,以及在更复杂的程序中可能在for循环内的更复杂的信息,更加独立。

我们典型的for循环需要三个输入。它们如下:

  1. 首先,我们需要声明我们将要递增或递减以计算我们循环的次数的变量。在这种情况下,我们将使用一个整数i,并给它一个初始值1。我们在这个初始语句后面加上一个分号。这不是一个函数调用;这是for循环的特殊语法。

  2. 特殊语法需要的第二个信息是我们需要评估每次重新开始循环时的条件语句。如果这个条件语句不成立,那么我们的for循环就结束了,我们继续在for循环块之后恢复我们的代码。在这种情况下,我们的条件语句将与while循环的条件语句相同。我们希望我们的for循环的最后一次迭代是当i等于100时,也就是当我们打印出100时。一旦i不再小于或等于 100,就是退出我们的for循环的时候了。

  3. 就像我们特别给for循环的第一个信息使我们不必处理循环范围之外的变量一样,我们将给for循环的最后一个信息取代我们在循环范围内手动递增或递减计数器。这是特殊的修改代码,无论我们在这里为for循环提供什么,都将在每次循环结束时运行。在这种情况下,我们只想在每次循环结束时递增i的值。我想你会同意,这个程序比我们的while循环要干净得多:

package forloops; 

public class Forloops { 
    public static void main(String[] args) { 
    /*  int i=1; 
        while(i <= 100) { 
            System.out.println(i); 
            i++; 
        }*/ 
        for(int i=1; i<=100; i++) 
        { 
            System.out.println(i); 
        } 
    } 
} 

现在,让我们检查一下它是否执行了相同的任务,即将值从1打印到100到我们的屏幕上,如下面的截图所示:

如果这个语句在我们的for循环的最开始执行,0就是正确的,但是这个语句在最后执行。

当我们在 Java 或任何编程语言中处理大数字和增量时,我们会遇到错误,就像我们刚刚遇到的那样,一错再错OBOE)错误。OBOE 是那种即使有经验的程序员也会遇到的小逻辑错误,如果他们不注意或者只是看错了一瞬间。学会识别 OBOE 的症状,例如,输出的行数比预期的多一行,将使我们能够更有效地追踪并找到它们。

摘要

在本章中,我们基本上看到了如何使用条件if...else语句来运行复杂的条件,使用诸如containscomplexboolean等函数。我们通过程序详细讨论了switchcasebreak的复杂性;此外,我们深入探讨了如何使用whiledo...whilefor循环的循环功能。

在下一章中,我们将看一下所谓的数据结构

第四章:数据结构

在本章中,我们将学习 Java 中一些最重要的数据结构。我们将研究数组是什么,以及当我们需要处理变量序列时它们如何有用。我们将在 NetBeans 中使用数组编写一个程序来理解它们的工作原理。本章还将介绍多维数组的概念。我们将编写一个程序,使用二维数组创建一个棋盘。

接下来,本章将说明 ArrayList 是什么,以及与数组相比,它们如何提供增强功能。最后,我们将看看Map数据结构,并在 NetBeans 中实现它。

更具体地,我们将涵盖以下主题:

  • 数组及其语法

  • 一个打印英文字母表的数组示例

  • 多维数组

  • 使用 2D 数组创建棋盘的程序

  • ArrayList 及其示例

  • 在 NetBeans 中的地图及其实现

使用数组

在本节中,我们将学习 Java 数组。数组是 Java 最基本和常用的数据结构。数据结构是一种工具,允许我们存储和访问信息序列,而不是使用单个变量。当我们在本地编程空间中需要一个特定的信息片段时,变量非常有用,但是当我们想要存储大量或复杂的信息集或系列时,就会使用数据结构。我们将从一些视觉学习模式开始本节,然后我们将进入 NetBeans IDE 编写一些实际的 Java 代码并使用数组。

声明和初始化数组

让我们首先看一下在 Java 中声明和初始化数组的语法。以下代码行将使一个数组产生,有足够的空间来容纳七个字符:

char[] arrayVar = new char[7]; 

在我们的赋值运算符(=)的左侧,语法看起来非常熟悉,与声明任何其他原始或对象时使用的语法非常相似。我们首先告诉 Java 我们要在这里声明什么类型的元素。在这种情况下,我们声明了一个字符数组。空方括号让 Java 知道,我们不是要创建一个单个字符变量,而是要声明一个数组类型变量,因为我们的数组就像任何其他变量一样。我们将通过数组的变量名本身访问数组的元素,而不是通过元素的单独变量名,因为它们被存储在数组中,我们不需要分配它们。告诉 Java 我们要创建什么类型的数组后,我们给我们的数组变量一个名称。我把这个叫做arrayVar

在我们的等号运算符右侧,情况看起来有些不同。您可能已经在过去看到new关键字的使用,当我们需要创建一个对象的新实例时,而不是原始元素。在 Java 中创建原始元素时,Java 知道需要多少内存空间来存储原始元素,无论其值如何。然而,对象和数组可能具有许多不同的大小要求。因为单个数组变量可以分配给不同长度的数组,所以当我们创建它们时,我们需要告诉 Java 为这些不同长度的数组中的每一个分配多少内存。因此,在创建对象或数组时,我们使用new关键字告诉 Java 应该设置多少内存空间来放置我们即将产生的东西,而那个东西是一个长度为七的字符数组。

在声明和初始化我们的七个字符数组之后,我们程序的本地内存中存在以下内容:

我们的数组基本上是一个足够大的内存块,可以存储七个单独的字符。

为数组分配值

当我们调用arrayVar变量时,我们的程序访问数组的位置。这使我们能够运行以下代码行:

arrayVar[2] = 'c'; 

我们的arrayVar变量基本上让我们可以访问七个不同的字符变量。当我们不想给我们的arrayVar变量分配一个新的数组时,我们可能会单独访问这些字符变量。我们只需使用arrayVar的变量名,后面跟着方括号,其中包括我们想要访问的单个字符的索引。请记住,当我们的计算机计算索引时,它们几乎总是从0开始。因此,在 Java 中,我们的七个字符数组具有这些索引:0123456。如果我们执行上面的代码行,同时将我们的arrayVar中索引2的值设置为c,我们将取出内存的第三个块,并将其值分配给字符c,如下图所示:

有时,当我们声明一个数组时,我们只想继续在代码中明确地为所有的内存块分配值。当我们想要这样做时,我们可以像明确声明原始类型一样,而不是使用new关键字并让计算机告诉它数组的长度,我们可以明确声明一个数组。例如,我们可以使用以下代码为我们的arrayVar变量做到这一点:

arrayVar = {'a', 'b', 'c', 'd', 'e', 'f', 'g'}; 

前面的语句将创建一个长度为七的数组,因为声明了七个元素,并且当然,它将相应地映射值:

现在,让我们跳入一些 Java 代码,并让数组开始工作。

NetBeans 中的数组示例

好了,我想现在是时候运用我们的新知识并编写一个计算机程序了。数组允许我们处理在单个元素级别处理起来会很麻烦的信息量。因此,我们将直接进入重要的内容,并创建一个很酷的计算机程序。数组是一个很大的逻辑步骤,如果你以前没有使用过类似的东西,可能需要一点时间来理解它们。好消息是,如果你通过了 Java 中的数组,你可能会很好地处理语言可以给你带来的其他任何东西。

我想要编写的程序将把英语字母表打印到屏幕上。当然,我们可以自己做所有这些,只需按照以下代码的方式进行:

System.out.println("abcdefg"); 

然而,使用这个方法相当令人昏昏欲睡,而且不会教会我们太多东西。相反,我们要编写的程序将学习、存储并打印出英语字母表。

为了做到这一点,我们需要运用我们对数组的新知识,我们对字符如何工作和在 ASCII 表上映射整数值的现有知识,以及一个for循环。

创建一个数组

让我们开始我们的编程,声明并初始化一个字符数组,用来存储英语语言的字符。因此,我们告诉 Java 我们需要一个变量来指向一个字符数组。我会把这个变量称为alpha。然后我们要求 Java 使用new关键字为26个字符分配内存空间,因为英语语言有 26 个字母:

char[] alpha = new char[26]; 

现在,如果你记得,字符值也可以映射到整数值。要找到这些值,我们将查找 ASCII 表。(您可以在www.asciitable.com上访问 ASCII 表。)

我们要找的值是97,小写字母a的整数值,这是英语语言中的第一个字符。因此,让我们在我们的程序中创建一个小注释,并将值97存储起来以备后用:

package alphabet; 

public class Alphabet { 
    public static void main(String[] args) { 
        // 97 
        char[] alpha = new char[26]; 
    } 
} 

创建一个 for 循环

现在让我们开始创建我们的for循环。我们的for循环将运行 26 次;每次运行时,它将取出英语字母表中的下一个字符,并将其放入我们的字符数组alpha中。

为了确保我们的for循环运行 26 次,我们应该声明一个计数变量,比如i,并将其设置为0,即(i=0)。接下来,让我们说我们的for循环应该继续运行,只要我们的计数变量的值小于26,也就是说,它应该在025之间取值(i<26)。最后,每次我们的for循环运行时,我们需要增加我们的计数变量的值,以便它每次都增加,经过 26 次迭代后,i<26语句将不再为真,我们的循环将在(i++)处停止:

for(int i = 0; i < 26; i++) 
{ 

} 

现在,在我们的for循环内部,我们将逐个为字符数组中的空格赋值。要访问其中一个空格,我们将使用分配给数组的变量的名称,即alpha,后跟方括号内的数字(或索引),以告诉 Java 我们想要为数组中的哪个字符赋值。

我们数组的索引应该在每次循环中都不同。这就是for循环的美妙之处。通过将我们的计数变量i0开始,我们可以使用它来映射到数组的索引。也就是说,我们可以使用alpha[i]逐个访问数组的元素。随着循环运行,我们的计数变量的值将从 0 到 25 变化。数组的索引值(因为计算机从零开始计数)也从 0 到 25 变化。

那么,我们为每个字符分配什么值,以便我们的计算机学会字母表呢?嗯,我喜欢这样想:当我们第一次运行循环时,当i0时,我们数组的第一个元素的值应该是97,这是字符a的整数值。现在,当我们应该将97+i作为数组中每个字符的值。当我们第二次运行循环时,i增加了一,我们将分配值 97 + 1,或98,这是字符b的整数值:

for(int i = 0; i < 26; i++) 
{ 
    alpha[i] = (char)(97 + i); 
} 

在这种情况下,Java 要求我们明确告诉它,我们希望将这个整数值转换为字符,然后存储它。

打印字母表

现在,要完成我们的程序,我们需要做的就是打印出我们的alpha数组。为此,让我们利用一个始终可访问的对象中的一个巧妙的函数,称为ArraysArrays.toString()函数将转换为字符串的单维数组(这是我们创建的数组的类型),可以转换为字符串:

public class Alphabet { 
    public static void main(String[] args) { 
        //97 
        char[] alpha = new char[26]; 

        for(int i = 0; i < 26; i++) 
        { 
            alpha[i] = (char)(97 + i); 
        } 

        System.out.println(Arrays.toString(alpha)); 
    } 
} 

现在,如果我们运行我们的程序,我们将看到 Java 以数组形式表示的英文字母:

如果您一直跟着做,那么您应该给自己一个坚实的鼓励。我们刚刚做了一些重活。

Java 中数组的默认初始化

现在,让我们回到理论中的其余部分。我之前误导了你,让你相信我们新创建的数组是用空内存空间填充的。实际上,当我们声明一个新的原始类型数组,即字符、整数、布尔值、浮点数等时,Java 会用默认值填充它。例如,我们的七个字符的新数组被七个空格字符填充,也就是如果您在键盘上按空格键会得到的结果:

同样,整数数组将填充七个零:

我建议您启动 Java IDE 并创建一些空的原始数组,并使用println将它们打印出来,以查看默认值是什么。

现在我们可以创建任何可用对象的数组。但是,与原始类型不同,对象在初始化为数组的一部分时不会设置默认值。这是一个重要的事实。

我们需要使用new关键字创建的任何内容都不会在数组中进行默认初始化。

假设出于某种原因,我们决定必须在数组中有七个Scanner对象。以下语句并不会为我们创建七个Scanner对象;它只是简单地设置了内存空间:

我们可以创建Scanner对象并将它们分配到这些内存空间,但如果在我们分配Scanner对象给内存位置之前尝试调用其中一个内存空间并使用 Scanner 特定的函数,我们的程序将崩溃。我们将得到所谓的NullReferenceException,这意味着 Java 要求虚无行为像一个Scanner对象。

多维数组

在 Java 中,我们最基本的数据结构是数组,它允许我们存储轻类型信息的序列,并通过内存中的单个位置访问这些信息。然而,有时数组不灵活,我们希望使用更强有力的组织数据结构,以便人类更容易理解和编写程序。在这种情况下,通常适合使用多维数组。

“多维数组”听起来是一个相当可怕的名字,但实际上它背后的概念非常基本。问题是如果我们创建一个数组的数组会发生什么?以下代码显示了如何做到这一点的语法:

char[][] twoDimArr = new char[3][7];

这行代码将创建一个二维多维数组。你会看到它非常类似于在正常情况下简单创建字符数组的语法,但在我们现在引用数组变量的每个实例中,Java 将需要两个信息(或两个索引)。前面的代码将告诉 Java 创建三个数组,每个数组都有足够的空间来存储七个字符或长度为七的三个数组:

为了巩固我们对这个概念的理解,让我们编写一个利用二维数组的 Java 程序。

在 NetBeans 中的多维数组示例

我们可以使用多维数组以抽象的方式存储信息,但最容易的学习方法可能是通过用二维数组表示实际的二维对象,比如国际象棋棋盘。

经典的国际象棋棋盘被分成黑色和白色方块;宽度为八个方块,高度为八个方块。我们即将编写的程序将在 Java 中存储一个虚拟棋盘,并正确标记黑色和白色方块。然后,在最后,我们将打印出这个棋盘,以便我们可以检查我们是否正确地编写了程序。

创建多维数组

让我们首先声明并初始化我们将要使用的数组。我们将使用字符数组来完成这个任务,给白色方块赋予字符值W,给黑色方块赋予字符值B。由于国际象棋棋盘是一个八乘八的网格,我们将声明一个包含八个数组的二维数组,每个数组应包含八个字符:

char[][] board = new char[8][8]; 

让我们通过将我们棋盘的尺寸存储在一个单独的位置来使某人更难无意中破坏。为此,只需创建一个名为boardDim的变量,为棋盘尺寸,将其赋值为8,然后在创建数组时引用它。数组将很乐意使用变量中的整数来初始化自己,让我们可以根据需要创建动态链接的数组。现在,如果有人想要扩大我们的国际象棋棋盘,他们只需要改变boardDim的值:

int boardDim = 8; 
char[][] board = new char[boardDim][boardDim]; 

为了给我们的方块分配适当的值,我们需要循环遍历这个数组,以便到达每个单独的节点并给它赋予我们想要的值。

使用嵌套循环进行多维数组

循环和数组非常合适,因为数组总是知道它们的长度,但单个for循环不能让我们有意义地循环遍历二维数组。for循环实际上只是沿着一个方向进行,而我们的二维数组有两个方向。

为了解决这个问题,我们将利用嵌套的for循环,或者for循环中的for循环。我们的外部for循环将依次循环每个数组,而内部for循环的工作将是循环遍历这些数组包含的节点。

创建for循环时的常见做法是使用整数变量i作为初始for循环,然后使用jk等变量作为后续for循环。然而,因为我们正在创建一个实际对象的棋盘,我将选择值y作为我们外部循环的计数变量。这是因为我们的循环正在沿着棋盘的y轴进行迭代。

如前所述,for循环和数组非常合适,因为数组知道它们的长度。我们可以简单地声明我们希望这个循环运行八次(y<8),但这不是良好的动态编程,因为如果有人改变了棋盘的大小,我们的程序现在就会出错。我们可以编写这个循环,使其适用于任何大小的棋盘。

为了做到这一点,我们不应该明确地说我们的循环应该运行八次,而是应该让它开始询问我们的数组有多长。要询问数组的长度,我们只需要写array.length,这将返回一个整数值。这是一个二维数组,所以简单地调用数组的名称来使用length变量将得到数组最外层段的长度。在这种情况下,我们正在询问我们的二维数组,“你有多少个数组?”为了完成这个for循环,我们只需要在每次运行后递增y。因此,我们的外部for循环将循环遍历我们的 2D 数组board包含的每个数组:

for(int y = 0; y < board.length; y++) 
{ 
} 

现在,让我们对内部循环做类似的事情。因为这个循环将遍历我们行的单个元素,所以对于x轴来说,x似乎是一个合适的变量名。因为我们的数组目前在两个部分中的长度相同,即一个八乘八的数组,简单地使用board.length语句,现在可以工作。但再一次,这不是良好的动态编程。如果有人通过更改我们的棋盘大小为八乘十,这个程序将不再正确执行。相反,在这个内部for循环执行的开始,让我们询问我们当前通过外部循环访问的数组有多长。这再次使我们的程序健壮,并允许我们适应棋盘的多种尺寸:

for(int x = 0; x < board[y].length; x++) 
{ 
} 

好的,我们程序的下一步是为数组中的每个节点分配字符值:黑色方块为B,白色方块为W。让我们首先编写代码使所有方块都是白色的。当我们执行双重for循环时,它将通过我们的二维数组中的每个节点。因此,每次我们执行内部for循环中的代码时,我们都是根据单个二维数组节点来执行的。为了获得这个节点,我们需要询问我们的board数组在第y行和第x列的位置是什么,然后我们将改变该节点的值:

for(int y = 0; y < board.length; y++) 
   { 
      for(int x = 0; x < board[y].length; x++) 
      { 
         board[y][x] = 'W'; 
      } 
   } 

为我们的棋盘分配不同的颜色

问题是,每次这个内部循环执行时,我们都希望节点的值不同,这样我们就得到了交替的白色和黑色方块的棋盘。为了帮助我们做到这一点,让我们在程序中添加另一个变量。它将是一个布尔变量,我们将其称为isWhite。如果isWhitetrue,那么我们添加的下一个方块将是白色;如果isWhite为 false,方块将是黑色。

为了编写代码,让我们使用一些if语句。首先,if(isWhite)代码术语检查isWhite是否为true。如果是,我们就在方块中放一个W。如果isWhitefalse,我们就在方块中放一个B代表黑色。要检查某事是否不是真的,我们可以在条件语句之前用感叹号来翻转任何布尔值。这对布尔值甚至条件语句都适用。

接下来,我们只需要翻转isWhite的值。好吧,利用我们对感叹号运算符的知识,它可以翻转布尔值的值,我们可以通过简单地将其值设置为其自身的倒数版本,将isWhite的值从true翻转为false或从false翻转为true

public static void main(String[] args) { 
   int boardDim = 8; 
   char[][] board = new char[boardDim][boardDim]; 
   boolean isWhite = true; 

   for(int y = 0; y < board.length; y++) 
   { 
       for(int x = 0; x < board[y].length; x++) 
       { 
           if(isWhite) board[y][x] = 'W'; 
           if(!isWhite) board[y][x] = 'B'; 
           isWhite = !isWhite; 
       } 
    } 
} 

不幸的是,这个程序还不够完美。事实证明,如果我们这样做,我们的棋盘将每一行都以白色方块开头,而真正的棋盘是每隔一行用不同颜色的方块交替的。

幸运的是,外部循环对棋盘的每一行运行一次。因此,如果我们在每一行的开头简单地给我们的isWhite布尔值添加一个额外的翻转,我们也会得到交替的行开头。如果我们这样做,我们需要将isWhite的初始值设为false,因为当外部循环第一次执行时,它将立即更改为true

public static void main(String[] args) { 

   int boardDim = 8;  
   char[][] board = new char[boardDim][boardDim]; 
   boolean isWhite = false; 

   for(int y = 0; y < board.length; y++) 
   { 
      isWhite = !isWhite; 
      for(int x = 0; x < board[y].length; x++) 
      { 
         if(isWhite) board[y][x] = 'W'; 
         if(!isWhite) board[y][x] = 'B'; 
         isWhite = !isWhite; 
      } 
   } 

打印棋盘

如果您迄今为止一直在跟进,请继续编写我们程序的最后一部分,一行代码来将我们的棋盘打印到屏幕上。实际上,我们需要的不仅仅是一行代码。我们可以使用println()函数以及arrays.toString()来将单个数组的内容打印到屏幕上,但是这种技术在二维或更高维数组中效果不佳。

因此,我们需要再次使用for循环来依次抓取每个数组,然后将它们打印到屏幕上。这很有效,因为println将自动换行,或者在我们打印每一行之间给我们一个新行。在这里,让我们使用传统的语法变量i来迭代我们的for循环:

您会注意到,Java 还不理解前面截图中显示的Arrays关键字;这是因为Arrays位于java.lang包中。当我们调用函数或类时,Java 不知道立即在哪里找到它,我们必须上网在 Google 上找到它时,这可能有点烦人。如果我们在 IDE 中工作,比如 NetBeans,有时会有一个查找常用包的快捷方式。在这种情况下,如果我们右键单击问题语句并转到“修复导入”,NetBeans 将浏览常用包并检查是否可以弄清楚我们在做什么:

在这种情况下,NetBeans 已经找到了Arrays类并为我们添加了导入语句:

import java.util.Arrays; 

现在,因为我们不想在每次for循环执行时尝试打印二维数组的内容(这样也不会很好),我们将告诉我们的println语句打印board[i]的内容,或者我们已经访问的二维数组中的单个数组:

public static void main(String[] args) { 
   int boardDim = 8;  
   char[][] board = new char[boardDim][boardDim]; 
   boolean isWhite = false; 

   for(int y = 0; y < board.length; y++) 
   { 
       isWhite = !isWhite; 
       for(int x = 0; x < board[y].length; x++) 
       { 
           if(isWhite) board[y][x] = 'W'; 
           if(!isWhite) board[y][x] = 'B'; 
           isWhite = !isWhite; 
       } 
   } 

   for(int i = 0; i < board.length; i++) 
   { 
       System.out.println(Arrays.toString(board[i])); 
   } 
} 

现在,让我们看看我们第一次是否做得对,并运行我们的程序:

哇!看起来我们做到了。有一个交替的白色和黑色的棋盘表示,以白色方块开始,并且行以正确的方式开始。现在可能看起来不起眼,但它的意义很大。我们基本上教会了我们的程序棋盘是什么样子。这是我们朝着创建更大的东西迈出的第一步,比如一个下棋的程序。

如果我们创建一个下棋程序(这有点超出了本节的范围,但我们可以在概念上讨论一下),我们可能希望我们的每个方块能够存储更多信息,而不仅仅是它们的颜色。例如,我们可能希望它们知道上面有什么棋子。为了实现这一点,我们可以利用三维数组。我们可以创建一个看起来像下面这样的数组,以便每个方块可以存储一个包含两个信息的数组,一个字符表示它的颜色,另一个字符表示它上面有什么棋子:

char[][][] board = new char[boardDim][boardDim][2]; 

这就是 Java 中多维数组的基础。

ArrayLists

当我们需要一个 Java 数据结构时,我们应该首先问自己是否简单的数组就足够了。如果我们可以使用一个简单的数组轻松整洁地编写我们的程序,那可能是保持程序简单的最佳选择。如果你正在编写必须尽可能快地运行并尽可能高效地使用内存的代码,数组也将几乎没有额外开销。但是,在今天的开发世界中,内存效率和速度对于普通程序来说真的不是问题,有时我们需要使用具有更多内置功能的数据结构,或者可能是为特定目的而设计的数据结构。

具有附加功能的数据结构称为 ArrayList。传统数组的一个弱点是,当我们实例化它们时,我们必须给它们一个特定的长度,因此我们必须知道我们希望数组有多大。ArrayList 基本上是一个包装在一些附加代码中的数组,这些代码导致数组的大小增加或减小,以始终保持与其包含的元素数量相同的大小。

NetBeans 中的一个 ArrayList 示例

要看到这个实例,让我们编写一个程序,如果我们只使用标准数组而不是 ArrayList,那么编写起来可能会更困难一些。我想编写一个程序,它将从用户那里获取一个输入字符串。它将存储这个输入字符串以及用户以前给它的每个其他输入字符串,然后每次用户输入一个新字符串时都打印它们出来。

这将是非常困难的,因为如果用户输入的字符串比数组设计的容量多一个,数组将在最好的情况下不接受字符串;在最坏的情况下,程序可能会崩溃。但是,我们的 ArrayList 对象将简单地调整大小以适应它当前持有的字符串数量。

创建一个 ArrayList

我们需要从导入java.util开始,因为java.utilScanner类(我们需要获取用户输入)和ArrayList类本身所在的地方。一旦我们声明了一个Scanner,我们稍后会更多地利用它,现在是时候声明我们的ArrayList了:

package echo; 

import java.util.*; 

public class Echo { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        ArrayList memory = new ArrayList(); 
    } 
} 

简单地声明ArrayList看起来很像声明任何其他对象。我们说出我们想创建的对象的类型。我们给它一个名字。我们使用new关键字,因为 Java 将不得不设置一些内存来创建这个对象,因为它不是原始的。然后,我们告诉 Java 实际创建对象。即使我们不会为我们的ArrayList创建提供任何参数,我们仍然需要在其后跟上双括号。这实际上是我们刚刚编写的有效代码,但通常当我们创建一个ArrayList时,我们会做更多的事情。

我们创建的ArrayList内存实际上将存储我们放入其中的任何类型的单个实体。这一开始听起来可能非常好,但老实说,在我们的程序中这不是一件好事。如果我们有 ArrayLists,或者任何数据结构,实际上存储了几乎任何东西,很容易感到困惑,如果我们觉得有必要这样做,要么我们正在做一些非常复杂的事情,要么更可能的是我们没有编写我们的代码如我们应该那样清晰。更重要的是,一旦我们在 ArrayList 中存储任何东西,我们就有可能绕过编译器并创建编译正常的代码。然而,另一种可能性是它会在运行时出错,导致那种在商业软件中非常糟糕的 bug,因为它们在人们实际使用时可能会出现问题。

为了解决这个问题,我们可以告诉我们的 ArrayList 只接受特定类型的信息。我们通过在ArrayList声明和实例化后跟随双字符括号,并在其中放置一个类型来实现这一点:

ArrayList<String> memory = new ArrayList<String>(); 

我们声明并使ArrayList数据结构成为可能,它只允许存储字符串。

获取用户输入

我们需要一个循环,这样我们的用户可以向程序输入多个字符串。现在,让我们只使用一个无限循环。它将永远运行,但在构建程序和调试程序时,我们总是可以手动停止它:

while(true) 
{ 

} 

每次循环运行时,我们都要使用 Scanner 变量reader上的nextLine()函数,从用户那里获取一个新的输入行,并将其存储在我们的 ArrayList 中。

当我们使用对象数据结构时,也就是说,具有自己的代码包装、函数和方法的数据结构时,通常不需要处理内存的各个索引,这可能非常好。相反,我们使用它们提供的函数来添加、删除和操作其中的信息。

在这种情况下,向 ArrayList 添加内容非常容易。ArrayList 中的add()函数将添加我们提供的任何输入,也就是说,只要它是一个字符串,就会将其添加到 ArrayList 包含的数组的末尾。因此,让我们添加以下代码行,它将请求用户输入一个新的字符串,然后将其放在我们的无限while循环内的 ArrayList 末尾:

memory.add(reader.nextLine()); 

打印用户输入的 ArrayList

现在,我们可以简单地使用println将我们的 ArrayList 打印给用户。请注意,println代码行不知道如何将 ArrayList 作为输入。实际上,它可能知道,但我们应该明确使用toString()函数,几乎每个 Java 对象都实现了它:

package echo; 

import java.util.*; 

public class Echo { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        ArrayList<String> memory = new ArrayList<String>(); 

        while(true) 
        { 
            memory.add(reader.nextLine()); 
            System.out.println(memory.toString()); 
        } 
    } 
} 

现在,当我们运行我们的程序时,我们将被提示输入一些用户输入,并且我们将看到输入被回显。如果我们给 Java 一些更多的输入,我们将看到更多的输入,并且旧的输入将被存储在我们的ArrayList中:

所以这很酷!我们已经构建了一个非常基本的程序,使用简单的数组写起来会更困难。

将控制权交给用户

ArrayLists 内含有很多强大的功能。我们可以将它们转换为数组,从数组创建它们,以及各种其他操作。如果我们去 Java 文档并在java.util下查找 ArrayList,我们可以找到它们的所有方法。让我们给我们的 ArrayList 程序添加一些功能,这样我就可以向您介绍一些常见的 ArrayList 方法。

ArrayLists 有一个不需要输入的函数,称为clear(),它将擦除我们的 ArrayList。我们可以利用这个函数来让我们的用户对我们的程序有一些控制。假设如果用户输入字符串CLEAR,我们想要擦除 ArrayList 中的所有信息。好吧,这是一个条件语句,所以我们使用if语句。我们将在我们的while循环内部使用以下if语句代码来实现这个功能:

if((memory.get(memory.size()-1)).equals("CLEAR")) memory.clear(); 

首先,我们需要检查刚刚添加到我们的 ArrayList 中的项目是否与字符串CLEAR相匹配。这个项目将位于最后,也就是说,它将是具有最高索引值的最后一个项目。不幸的是,ArrayList 没有实现lastItem()函数,但我们可以通过将两个 ArrayList 函数get()size()组合在一起来创建一个自己的函数。

首先,为了从 ArrayList 中获取一个项目,我们利用get()函数。请注意,get()与我们访问传统数组中的项目时会使用的方括号非常相似。此外,get()函数将接受一个整数值,并将该整数映射到包含在我们的 ArrayList 中的数组的索引。

因此,要获取我们的 ArrayList 中的最后一个项目,我们需要知道 ArrayList 中有多少个项目。然后,我们想从该值中减去一个,因为长度为 7 的数组的最后一个索引将是 6,因为数组从零开始计数。要获取我们的 ArrayList 中有多少个项目,我们使用size()函数,它不需要参数,只是给我们一个整数,即数组的大小,即它包含多少个项目。我们从该值中减去1,以便我们可以正确访问最后一个索引,而不是其后面的索引,它可能包含任何内容。然后,我们将整个memory.get(memory.size()-1)块,它访问我们的ArrayList的最后一个项目,用括号括起来。

我们刚刚括起来的if语句块为我们获取了一个字符串对象。我们知道可以使用equals()方法来比较字符串。实际上,我们可以从这个代码块返回的字符串对象中调用该方法,即使我们还没有为它分配一个特定的变量名。对象存在,即使我们没有它们的名称,如果我们刚刚从其他地方返回它们,我们可以调用它们的方法,并且可以做任何我们喜欢的事情。

while(true) 
{ 
    memory.add(reader.nextLine()); 
    if((memory.get(memory.size()-1)).equals("CLEAR")) 
        memory.clear(); 
    System.out.println(memory.toString()); 
} 

因此,这是一个我们刚刚写的非常疯狂的语句,但只要我们写得正确,当我们的用户在程序中输入CLEAR时,我们将擦除 ArrayList。

写完这段代码后,我们可以编写非常类似的代码,为我们的用户提供不同的功能选项。让我们也允许用户输入END。目前,我们处于一个将无限循环直到我们手动关闭它的程序中。但是通过使用break Java 关键字,它将使我们跳出我们所在的任何循环,或者如果我们在一个函数中,它将使我们跳出该函数,我们可以使这个循环可以被打破。这样,我们可以让用户基本上关闭我们的程序,因为一旦我们离开这个循环,就没有更多的代码可以执行,我们的程序将结束:

public static void main(String[] args) { 
    Scanner reader = new Scanner(System.in); 
    ArrayList<String> memory = new ArrayList<String>(); 

    while(true) 
    { 
        memory.add(reader.nextLine()); 
        if((memory.get(memory.size()-1)).equals("CLEAR")) { 
            memory.clear(); 
        } 
        if((memory.get(memory.size()-1)).equals("END")) 
        break; 
    } 
    System.out.println(memory.toString()); 
} 

在使用break语句时要小心。确保这样做是有意义的,因为如果你在阅读别人的代码时,它们可能会让人有点困惑。它们会打破并跳转控制流到各个地方。

所以让我们运行这个程序,看看会发生什么。我们将从给我们的程序一些输入开始,并构建 ArrayList:

现在让我们尝试输入CLEAR并检查它是否清空了我们的 ArrayList。哦,不!我把它弄坏了:

这实际上是一个非常有趣的错误。我实际上犯了这个错误;这不是预先计划的。我会留下它,因为这对我们来说是一个很好的学习经验。它还表明,即使你是一名经验丰富的程序员,你也会犯错误。例如,我们应该尽可能使用带类型的 ArrayList,这样我们就可以轻松地找出并纠正我们的错误。

分析 ArrayIndexOutOfBoundsException

我们的程序抛出了ArrayIndexOutOfBoundsException。这意味着我们试图访问我们的memory数组没有访问权限的内存。具体来说,我们试图查看数组索引-1 处的内容。由于数组从索引 0 开始,它们没有任何内容在索引-1 处。计算机内存的任何部分都可能在那里,出于安全原因,程序不允许随意查看计算机的内存。那么,为什么会发生这种情况?为什么我们要求查看数组的索引-1,这永远不会是有效的数组索引?

嗯,我们第一个实现清除 ArrayList 功能的if语句执行得很好。我们的程序看到了我们的CLEAR命令,理解了我们对数组索引的第一次查看,并清空了数组。

紧接着,我们要求程序再次检查添加到数组中的最后一项,使用第二个if语句。当我们这样做时,我们执行了memory.size()-1。首先,我们询问 Java 关于我们的 ArrayList 的大小。因为我们刚刚清空了 ArrayList,Java 告诉我们 ArrayList 的大小为零,里面什么也没有。然后我们从这个值中减去 1,得到-1。然后,我们在这个-1 值上运行memory.get()。因此,我们要求 Java 查看数组索引-1 处的内容,此时 Java 说:“哇!你在干什么?这不好,我要崩溃了!”

那么,我们该如何解决这个问题呢?嗯,我们可以做一些事情。我们应该在运行第二个if语句中的函数之前检查并确保我们的数组不为空。这个选项看起来比我想要的代码行数多一些。这并不是不可逆转的,我鼓励你尝试并实现比这更好的解决方案。

目前,为了让我们的程序快速启动并且不崩溃,让我们将一对if块改为if...else语句如下:

while(true) 
{ 
    memory.add(reader.nextLine()); 
    if((memory.get(memory.size()-1)).equals("CLEAR")) { 
    memory.clear(); 
    } 
    else { 
        if((memory.get(memory.size()-1)).equals("END")) 
        break; 
    } 
    System.out.println(memory.toString()); 
} 

我们将第二个if语句嵌入了else块中。这将阻止我们连续运行两个if块。如果我们的第一个if语句评估为真并且我们的清除语句被执行,那么我们将不会检查第二个if语句。

现在,如果我们运行程序并输入一些胡言乱语来构建我们的 ArrayList,然后输入CLEAR,我们将正确地得到一个空的 ArrayList 的响应:

我们永远不会在大小为 0 的数组上触发第二个if语句,因为我们总是会在之前向数组中添加一行。

现在,让我们祈祷并检查END输入是否有效:

它确实会!break命令专门用于跳出循环和函数,所以即使我们将其嵌套在 if 和 else 语句中,它仍然会将我们从while循环中跳出来。

我认为我们遇到的小问题是一个很好的学习经验。我们遇到的错误实际上是一个非常有趣的错误。尽管如此,我希望你已经看到不同的数据结构有不同的用途。

地图

在本节中,我们将研究 Java 的Map数据结构。我想从一堆已经格式化的信息开始,所以我自己创建了一个小程序。你可以在本书的附属文件中找到以下程序。仔细查看它,确保你理解它的工作原理:

package maps; 
import java.util.*; 
public class Maps { 
    public static void main(String[] args) { 
        String[] allNames =   
            //<editor-fold desc="raw names data"> 
            {"Jane", "Addams", 
            "Muhammad", "Ali", 
            "Stephen", "Ambrose", 
            "Louis", "Armstrong", 
            "Joan", "Baez", 
            "Josephine", "Baker", 
            "Eleanor", "Roosevelt", 
            "Frank", "Sinatra" 
            }; 
            //</editor-fold> 
        String[] firstNames = new String[allNames.length/2]; 
        String[] lastNames = new String[allNames.length/2]; 
        for(int i = 0; i < allNames.length; i++) 
        { 
            /*This if statement checks if we are in an EVEN      
            NUMBERED iteration  
            % is the "mod" or "modulus" operator...  
            it returns the remainder after we divide number1 by      
            number2)*/ 
            if(i % 2 == 0)  
            { 
                //We are in an even number iteration - looking at      
                a first name 
                firstNames[i/2] = allNames[i]; 
            } 
            else 
            { 
                //We are in an odd number iteration - looking at a   
                last name 
                lastNames[i/2] = allNames[i]; 
            } 
        } 
        System.out.println(Arrays.toString(firstNames)); 
        System.out.println(Arrays.toString(lastNames)); 
    } 
} 

我假设我们还不熟悉文件输入和输出,所以我把我们通常想要存储在文件中或其他更可管理的地方的所有数据都放在了我们程序的代码中。我创建了一个名为allNames的字符串数组,它是一组名人的名字。他们各自的名和姓也被分开。所以亚当斯是数组的前两个元素。她的名allNames[0]的一部分,然后亚当斯,她的姓,是在allNames[1],以此类推,数组中的每两个元素是一个人的名和姓。

这也是我向你展示一个很棒的小功能的好机会,这个功能在大多数 IDE 中都可以使用。如果我们的 IDE 经常支持这样的功能,我们可以通过在代码的注释中放置对它们的指令来与它们交流。因为这些指令被注释掉了,它们不会以任何方式影响我们的 Java 代码的编译和运行,但我们可以与 IDE 交流。程序中的以下指令和它的结束指令告诉 NetBeans 我们想要它将它们之间包含的代码分隔开:

//<editor-fold desc="raw names data"> 
. 
. 
. 
//</editor-fold> 

现在,我们可以使用左侧的小框来展开和收缩代码块,就像下面的截图所示:

它并没有使代码消失;它只是把它从我们面前隐藏起来,这样我们就可以在不弄乱屏幕的情况下开发它:

现在,让我们来看一下我写的程序的一个非常快速的解释,以开始这一部分。我们有一个名为allNames的字符串数组,其中包含许多名人的名和姓。我写的程序简单地循环遍历这个数组,并确定它是在查看名字还是姓。然后它将这些名字放在它们自己的单独的数组中。最后,当我们打印出这些数组时,我们有两个单独的数组:一个是名字的数组,一个是姓的数组。这些数组的关系是,因为我们将它们按顺序放入了两个单独的数组(firstNameslastNames)中,所以数组的索引是匹配的。因此,在firstNames[0]lastNames[0],我们有简·亚当斯的名字和姓。

现在,我想扩展这个程序,并将所有这些信息放在一个单一的数据结构中:一个 JavaMap。在创建这样一个 Map 时,我们让它知道一个集合之间的关系,我们称之为键,另一个集合,我们称之为值,这样每个键都映射到值。这将允许我们向我们的程序提问,比如,“给定一个名人的姓,与之相关联的名字是什么?”

创建一个 Map

首先,我已经导入了java.util,那里有Map接口。接下来,我将删除打印firstNameslastNames数组的最后两个println语句。相反,在我们的代码中的这一点上,当我们的firstNameslastNames数组已经设置好时,让我们开始构建我们的Map。为此,添加以下代码行:

Map<String, String> famousPeople = new HashMap<>(); 

我们首先使用Map关键字,然后,与大多数数据结构一样,我们告诉 Java 我们的Map将要接受什么类型的信息。Map 接受两组信息,所以我们必须给它两个以逗号分隔的信息类型。第一个信息类型是 Map 的键的信息类型,第二个信息类型是 Map 的值的类型。

我们将使用lastNames作为我们的键,因为我们不希望我们的Map在一个键中存储多个值,而且我们很少会有多个相同的姓氏。此外,对我们来说,询问名为 Addams 的名人的名字比询问名为 Jane 的名人的姓氏更有价值,后者可能更多。无论如何,lastNames的数据类型是StringfirstNames的数据类型也是String

接下来,我们给我们的新Map变量取一个名字:famousPeople。然后,我们通过实例化来使我们的Map存在。为了做到这一点,我们使用new关键字。Map实际上不是一个对象,它是我们称之为接口。在大多数情况下,我们以相同的方式与接口和对象交互,但我们不能简单地声明一个接口的实例。相反,接口是我们放在对象之上的功能的额外包装,就像 ArrayLists 为数组添加了额外的功能一样。

因此,要创建一个新的Map,我们需要一个更简单的对象类型,我们可以在其周围包装Map接口。这方面的一个很好的候选者是HashMap。因此,我们创建我们的HashMap并将我们的 Map 变量famousPeople分配给它。现在,我们将与这个famousPeople变量交互,就像它是一个具有所有Map功能的对象一样。此外,如果我们愿意,我们也可以在这个对象上调用HashMap功能。

虽然这有点超出了本节的范围,但接口的强大之处在于我们可以将它们分配给不同类型的对象,从而为否则不同的对象类型提供共同的功能。但是,目前,我们主要只对 Java Maps 的功能和功能感兴趣。您会注意到,我们不必明确告诉 Java 我们的HashMap将采用什么类型。这实际上是一种风格选择;如果我们愿意,我们可以明确声明HashMap将采用的类型:

Map<String, String> famousPeople = new HashMap<String, String>(); 

然而,由于我们只会根据其作为Map的功能与我们的HashMap进行交互,通过变量famousPeople与我们的HashMap进行交互时,我们只需要保护自己免受添加除字符串以外的任何东西的影响,这样就可以了。

为我们的 Map 分配值

一旦我们设置了我们的Map,就该是时候填充它的信息了。对此,我认为使用for循环是合适的:

for(int i = 0; i < lastNames.length; i++) 
{ 
    famousPeople.put(lastNames[i], firstNames[i]); 
} 

我们需要向我们的 Map 添加许多信息对,即一个键和一个值,等于这些数组中的任何一个的项目数。这是因为它们的长度相同。因此,让我们设置一个for循环,遍历从i到(lastNames-1)的每个索引。i值将映射到lastNames数组的索引,因为firstNames数组的长度与lastNames数组的长度相同,它们也将映射到firstNames数组的索引。

现在,对于每个i,我们将执行我们的 Map 的put()函数。put()函数类似于add()函数。它将信息插入到我们的 Map 中。但是,这个函数期望两个信息。首先,它期望我们的键,即我们当前在lastNames中查看的值,然后它期望相关的值,即我们在firstNames中查看的值。每次我们在我们的for循环中执行famousPeople.put(lastNames[i], firstNames[i]);这行代码时,我们将向我们的Map添加一个新的键值对。

从我们的 Map 中获取信息

一旦我们设置了Map,程序中已经包含了所有信息,我们只需要问一些问题,确保我们得到正确的回答:

System.out.println(famousPeople.get("Addams")); 

我们使用get()函数来询问我们的Map它设计来回答的基本问题,“与给定键配对的值是什么?”因此,让我们问我们的Map,“与Addams配对的值是什么?”,或者更容易理解的英语术语,“在我们的 Map 中,姓氏是Addams的人的名字是什么?”当我们运行这个程序时,我们得到了预期的结果,即Jane

让我们再运行一次,以确保我们没有犯任何愚蠢的错误。让我们看看当我们输入Sinatra时,我们的程序是否会回答Frank

System.out.println(famousPeople.get("Sinatra")); 

确实如此!

虽然我们可以通过简单地循环遍历数组来编写这样的程序(当我们获得用户输入时找到lastName,存储该索引,并从firstNames获取它),但我们的 Map 接口基本上为我们做到了这一点。也许更重要的是,当我们与其他程序员合作或查看我们昨天没有编写的代码时,当我们看到Map时,我们立即理解它的预期目的以及它实现的功能。在几乎所有情况下,编写能够正常工作的代码同样重要,因为它是合理的,并且将被未来可能遇到代码的其他人理解。

总结

在本章中,我们讨论了数组,并举了一个使用数组打印英文字母表的例子。接下来,我们看了多维数组,并编写了一个创建二维棋盘的程序。

我们介绍了 ArrayList 是什么,以及它如何增强数组的功能。我们还编写了一个使用具有功能的 ArrayList 的程序,这在使用数组实现将会相当困难。最后,我们看了 Maps 并实现了一个例子以更好地理解它。

在下一章中,我们将详细讨论 Java 函数。

第五章:函数

在本章中,我们将从讨论 Java 程序基础知识中使用的一些基本概念和术语开始。你将通过简单的程序学习所有这些概念。你将了解到至关重要的 Java 方法。如果你是一名有经验的程序员,你可能以前遇到过函数。随着这些基本概念的进展,你将更多地了解高级 Java 函数。以下是我们计划在本章中涵盖的主题:

  • Java 函数的基础知识

  • 方法

  • 高级 Java 函数

  • 操作 Java 变量

Java 函数的基础知识

在 Java 中,“函数”和“方法”这两个术语基本上是可以互换使用的,而“方法”是更加技术上正确的术语,你会在文档中看到。

方法

方法是一种工具,允许我们打破程序的控制流。它们让我们声明一些小的子程序,有时我们可以把它们看作更小的程序,我们可以在我们的程序中引用它们,这样我们就不必把我们程序的所有逻辑代码都写在一个单一的块中:

public class TemperatureConverter { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        char inputType; 
        char outputType; 
        float inputValue; 
        float returnValue; 

        System.out.print("Input type (F/C/K): "); 
        inputType = reader.next().charAt(0); 
        System.out.print("Output type (F/C/K): "); 
        outputType = reader.next().charAt(0); 
        System.out.print("Temperature: "); 
        inputValue = reader.nextFloat(); 
    } 
} 

方法的一个例子是Scanner类中的.next方法。在我写的这个程序中,我们不必教Scanner对象如何获取用户输入的下一组数据,我只需从过去某人编写的类中调用next方法。这将把可能是几百行程序的东西转换成大约 22 行,如前面的代码所示。

通过编写我们自己的方法,我们可以通过将复杂的挑战分解成更小、更易管理的部分来解决它们。正确模块化并使用方法的程序也更容易阅读。这是因为我们可以给我们的方法起自己的名字,这样我们的程序就可以更加自解释,并且可以使用更多的英语(或者你的母语)单词。为了向你展示方法的强大之处,我已经计划了一个相当复杂的程序,今天我们要写这个程序。

温度转换程序

我们的目标是创建一个温度转换程序,我已经为我们设置了程序的输入部分:

public class TemperatureConverter { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        char inputType; 
        char outputType; 
        float inputValue; 
        float returnValue; 

        System.out.print("Input type (F/C/K): "); 
        inputType = reader.next().charAt(0); 
        System.out.print("Output type (F/C/K): "); 
        outputType = reader.next().charAt(0); 
        System.out.print("Temperature: "); 
        inputValue = reader.nextFloat(); 
    } 
} 

到目前为止,这个程序从用户那里获取了三条信息。第一条是温度类型:F代表华氏度,C代表摄氏度,K代表开尔文。然后它获取另一种温度类型。这是我们的用户希望我们转换到的类型;再一次,它可以是华氏度、摄氏度或开尔文。最后,我们从用户那里获取初始温度的值。有了这三条输入,我们的程序将把给定的温度值从华氏度、摄氏度或开尔文转换为用户所需的温度类型。

这是一个具有挑战性的程序,原因有两个:

  • 首先,因为有两组三个用户输入,所以有六种可能的控制流情况。这意味着在最坏的情况下,我们可能不得不写六个if...else块,这将很快变得笨拙。

  • 第二个挑战是进行实际的转换。我已经提前查找了三种温度转换的转换数学,即华氏度到摄氏度,摄氏度到开尔文,和开尔文到华氏度:

package temperatureconverter; 

import java.util.*; 

// F to C: ((t-32.0f)*5.0f)/9.0f 
// C to K: t+273.15f 
// K to F: (((t-273.15f)*9.0f)/5.0f)+32.0f 

public class TemperatureConverter { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        char inputType; 
        char outputType; 
        float inputValue; 
        float returnValue; 

正如你所看到的,虽然这不是困难的数学问题,但它肯定是笨拙的,如果我们开始在程序中到处复制和粘贴公式,我们的程序看起来会非常疯狂。你还应该注意,在前面的评论部分中,有三种转换,我们可以做出这个程序将被要求做的任何可能的转换。这是因为这三种转换创建了一个转换的循环,我们可以通过其中一个中间方程从一个特定类型到任何其他类型。

说了这么多,让我们直接开始编写我们的程序吧。

设置控制流

我们需要做的第一件事是设置一些控制流。正如我之前提到的,有六种可能的情况,可能会诱人地为每种可能的输入和输出类型设置六个if语句。不过这会有点笨拙,所以我有一个稍微不同的计划。我将不同的情况转换为每种可能的类型配对,首先要做的是将用户给出的初始温度值转换为摄氏度值。在我这样做之后,我们将把摄氏度值转换为用户最初寻找的类型。可以使用以下代码块来完成这个操作:

System.out.print("Input type (F/C/K): "); 
inputType = reader.next().charAt(0); 
System.out.print("Output type (F/C/K): "); 
outputType = reader.next().charAt(0); 
System.out.print("Temperature: "); 
inputValue = reader.nextFloat(); 

设置控制流的优势在于让我们完全独立地处理两个用户输入。这使得我们的程序更加模块化,因为我们在开始下一个任务之前完成了一个任务。

因此,为了进行这个初始转换,我们需要利用switch语句:

public static void main(String[] args) { 
    Scanner reader = new Scanner(System.in); 
    char inputType; 
    char outputType; 
    float inputValue; 
    float returnValue; 

    System.out.print("Input type (F/C/K): "); 
    inputType = reader.next().charAt(0); 
    System.out.print("Output type (F/C/K): "); 
    outputType = reader.next().charAt(0); 
    System.out.print("Temperature: "); 
    inputValue = reader.nextFloat(); 

    switch(inputType) 
} 

我们将在inputType字符变量之间切换,该变量告诉我们用户给出的温度类型是华氏度、摄氏度还是开尔文。在switch语句内部,我们将操作inputValue,其中存储着温度的值。

探索单独的情况-C、K 和 F

所以我想我们需要为每种可能或有效的输入类型编写单独的情况,即大写F代表华氏度,C代表摄氏度,K代表开尔文。我们可能还需要处理一个default情况。让我们先写default情况。我们将使用System.exit并以1退出,这在技术上是一个错误代码:

switch(inputType) 
{ 
    case 'F': 
    case 'C': 
    case 'K': 
    default: 
        System.exit(1); 

System.exit基本上退出我们的程序。它告诉程序停止执行并传递给操作系统或者更高级的东西。

在这种情况下,程序将停止。因为这是default情况,我们只期望在用户未能输入FCK时进入它,这些是我们有效的输入类型。现在,让我们处理每种输入类型。

摄氏类型

我们将在所有情况下使用摄氏度作为我们的第一个转换点,所以如果用户输入了摄氏值,我们可以直接跳出这种情况,因为inputValue的值对我们来说已经可以了。

switch(inputType) 
{ 
    case 'F': 
    case 'C': 
        break; 
    case 'K': 
        default: 
            System.exit(1); 

如果用户给出了华氏值怎么办?好吧,让我们滚动到代码的顶部;你会看到我们有一个明确的从华氏到摄氏的转换:

// F to C: ((t-32.0f)*5.0f)/9.0f 
// C to K: t+273.15f 
// K to F: (((t-273.15f)*9.0f)/5.0f)+32.0f 

我们可以采用前面的代码块,我已经使其非常适合 Java,并只需更改此输入变量的值为其值上运行的转换语句。因此,我们将用输入变量替换t占位符:

switch(inputType) 
{ 
    case 'F': 
        inputValue = ((inputValue-32.0f)*5.0f)/9.0f; 
        break; 
    case 'C': 
        break; 
    case 'K': 
    default: 
        System.exit(1); 
} 

这将正确地存储原始华氏值的摄氏等价值在这个变量中。

开尔文类型

我们可以对开尔文情况做类似的事情。我们没有一个明确的从开尔文到摄氏的转换,但我们知道如何将开尔文转换为华氏,然后再将华氏转换为摄氏。所以我们可以用以下方式做一些事情:

switch(inputType) 
{ 
     case 'F': 
         inputValue = ((inputValue-32.0f)*5.0f)/9.0f; 
         break; 
     case 'C': 
         break; 
     case 'K': 
         inputValue = ((((((inputValue-273.15f)*9.0f)/5.0f)+32.0f) -   
         32.0f)*5.0f)/9.0f; 
     default: 
         System.exit(1); 
} 

在前面的代码中,我们将开尔文值转换为华氏值,用括号括起来,并对其进行华氏到摄氏的转换。

现在这在技术上是一行功能性的代码。如果我们运行程序并输入一个开尔文输入情况,它将正确地将开尔文值转换为摄氏度值。但是,让我说,如果我是一个程序员,我在工作中遇到这样一行代码,特别是没有任何解释的代码,我是不会很高兴的。这里有很多魔术数字-数字在一般情况下真的是信息;这并不是以任何方式自解释的。当然,作为原始程序员,至少当我们写它时,我们记得我们的目标是将开尔文值转换为摄氏度值;然而,对于任何没有时间坐下来查看整个程序的其他人来说,这真的是不可理解的。那么有没有更好的方法来做到这一点?是的,绝对有。

华氏度类型

现在让我们尝试理解华氏温度的情况。考虑以下代码:

inputValue = ((inputValue-32.0f)*5.0f)/9.0f; 

上面的代码行比我们的开尔文情况好一点,因为它包含的数字更少,但从任何意义上来说,它仍然不够友好。那么,如果在我们最初实现这个程序时,我们可以提供真正对程序员友好的通信,会怎么样呢?如果我们不是在那里打印出等式,而是把等式放在程序的其他地方并调用一个华氏度到摄氏度的函数呢?

inputValue = fToC(inputValue); 

现在我们只需输入 fToC 来保持简洁。这对于查看我们的程序的人来说更有意义。

我们可以在这里做类似的事情来处理开尔文情况:

inputValue = fToC(kToF(inputValue)) 

如果我们想的话,我们可以调用一个开尔文到摄氏度的函数(kToC),或者如果我们甚至不想写那个,我们可以在我们的 inputValue 变量上调用一个开尔文到华氏度的函数,然后在此基础上调用 fToC 函数。这就是我们最初所做的所有数学概念上的事情,只是我们已经抽象出了那些数字,并把它们放在了程序的其他地方。这对程序员来说更友好。假设我们在数学上犯了一个错误,另一个程序员想要检查它。他们只需要找到我们即将编写的函数,比如 fToCkToF,然后他们就可以深入了解所有的细节。因此,当然,我们确实需要编写这些函数。

当我们创建一个新函数时,我们实际上是在当前的函数或方法之外进行的:

public static void main(String[] args) { 

目前,我们在程序的 main 方法中,这是一个特殊的方法,程序从这里开始执行。因此,为了编写我们的华氏度到摄氏度函数,我们将退出该方法并声明一个全新的方法;基本上,我们正在教我们的程序如何运行一个名为 fToC 的新程序:

public static fToC() 

现在,继续在你的方法前面使用 public static 关键字。一旦我们真正进入 Java 的面向对象的特性,这些关键字将非常重要,但现在,我们将在我们声明的所有方法上使用它们。

关于我们接下来计划如何处理程序的更详细解释,让我们尝试更详细地分割程序,分成两部分。

执行程序的第一部分

您标准的 Java 方法在我们给它一个名称之前还有一个关键字,那就是这个方法将返回的信息类型:

public static float fToC() 
{ 
} 

例如,我们希望能够在我们的开尔文到华氏度函数上调用fToC。当我们这样做时,我们基本上将我们的开尔文到华氏度函数的结果视为自己的浮点变量。这表明我们在这些函数中寻找的返回类型是float数据类型。这意味着当这些小程序执行完毕时,它们将向我们调用它们的main方法抛出一个浮点值。在命名函数之后,我们在其前面的函数声明中跟随两个括号。在这些括号之间,我们将告诉我们的程序这个函数需要运行的信息。我们通过基本上创建一些变量来做到这一点,如下面的代码块所示:

public static float fToC(fVal) 

我们将需要一个变量,我将其称为fVal,因为我们从华氏度值开始。在每个输入变量之前,我们还需要告诉我们的程序那将是什么类型的信息;这样人们就无法不正确地调用我们的函数并传递诸如字符串之类的东西,这是毫无意义的。

public static float fToC(float fVal) 
{ 
} 

因此,我们要告诉我们的函数,为了运行,它需要以给定的float信息作为输入进行调用。在我们之前编写的函数中,它们实际上存在于程序中。您会看到我们这样做:我们将inputValue或用户最初给我们的温度值的值作为这些函数的输入。

现在,我们需要我们的fToC函数,我们的华氏度到摄氏度函数,在代码中对fVal变量执行一些计算,其中将包含用户输入的温度值。由于我们从华氏度到摄氏度,我们可以只需复制并粘贴程序顶部的字符串,并将fVal替换为t

public static float fToC(float fVal) 
{ 
    fVal = ((fVal-32.0f)*5.0f)/9.0f; 
} 

现在,我们可能会诱惑我们的函数执行此操作来更改此变量的值。虽然我们当然可以这样做,但这不会给我们带来我们需要的结果。当我们的程序执行inputValue = fToC(inputValue);这行代码并运行我们的fToC函数时,将inputValue作为其输入变量,这个变量实际上不会降到我们函数的代码行中。相反,Java 只是复制inputValue的值并将其存储在我们的新变量中,如下面的代码块所示:

public static float fToC(float fVal) 
{ 
    fVal = ((fVal-32.0f)*5.0f)/9.0f; 
} 

因此,我们对这个fVal变量所做的更改不会映射到我们的inputValue变量。幸运的是,我们明确地将inputValue的值更改为我们现在编写的函数返回的值。一旦我们准备退出函数的执行,我们可以让它丢弃任何与我们告诉 Java 此函数将返回的值类型相等的值。我们使用return关键字来做到这一点,后面跟着计算为我们的情况下浮点值的任何语句。因此,当我们的fToC函数在inputValue上运行时,它将打印出与存储在输入变量中的初始华氏值等效的浮点数:

public static float fToC(float fVal) 
{ 
    return ((fVal-32.0f)*5.0f)/9.0f; 
} 

一旦我们编写了其中一个函数,编写其他类似的函数就变得非常容易。要编写我们的开尔文到华氏度的函数,我们只需要做同样的事情,但在这种情况下,我们需要采用我们的开尔文到华氏度转换方程并更改变量的名称。如果我们愿意,我们可以称之为fVal-kVal只是更具说明性,并返回该结果:

public static float fToC(float fVal) 
{ 
    return ((fVal-32.0f)*5.0f)/9.0f; 
} 
public static float kToF(float kVal) 
{ 
    return (((kVal-273.15f)*9.0f)/5.0f)+32.0f; 
} 

这是我们程序的第一部分,我们将用户提供的任何值转换为摄氏度值。到目前为止,这比使用六个if语句更加优雅,但我们只写了程序的一半。

执行程序的第二部分

一旦我们完成了摄氏度的转换,我们将使用另一个switch语句。这一次,我们将在outputType上使用它,用户告诉我们他们想要看到等值的温度类型,或者在哪种温度类型下看到等值。我们的情况将看起来非常类似于switch语句的前半部分;然而,这里我们不是将所有东西转换为摄氏度,而是总是从摄氏度转换。同样,这意味着C情况可以在我们转换为摄氏度的任何情况下简单地中断,然后我们不再需要从摄氏度转换:

// F to C: ((t-32.0f)*5.0f)/9.0f 
// C to K: t+273.15f 
// K to F: (((t-273.15f)*9.0f)/5.0f)+32.0f 

现在,我们明确的情况是摄氏度到开尔文的转换。我们知道这个公式,多亏了我们在代码顶部的小抄;我们可以很快地构建一个函数来做到这一点。我们将这个函数称为cToK;这是我们的变量名,这是逻辑:

public static float fToC(float fVal) 
{ 
    return ((fVal-32.0f)*5.0f)/9.0f; 
} 
public static float kToF(float kVal) 
{ 
    return (((kVal-273.15f)*9.0f)/5.0f)+32.0f; 
} 
public static float cToK(float cVal) 
{ 
    return cVal+273.15f; 
} 

一旦我们声明了我们的cToK函数,我们可以在inputValue上调用它,因为inputValue现在存储了修改后的原始输入值,这将是一个摄氏度数字,要转换为开尔文值:

case 'K': 
    inputValue = cToK(inputValue); 

类似于我们将开尔文转换为华氏度再转换为摄氏度的方式,当我们将所有东西都转换为摄氏度时,我们可以通过从摄氏值获取一个开尔文值来获得一个华氏输出。然后,我们可以使用开尔文转换为华氏度的函数将这个开尔文值转换为华氏度:

case 'F': 
    inputValue = kToF(cToK(inputValue)); 
    break; 
case 'C': 
    break; 
case 'K': 
    inputValue = cToK(inputValue); 
    break; 
default: 
    System.exit(1);  

这是我们程序的第二部分。仍然只有两行真正的代码可能会让任何人停下来,它们都相当容易理解。然而,我们程序的所有逻辑和功能对于一个好奇的程序员来说仍然是可访问的,他想要重新访问它们:

    } 
    System.out.println(inputValue); 
} 

程序的最后一步

我们可以使用 println 来结束我们的程序,输出 inputValue,它现在应该包含正确的转换。让我们运行这个程序,输入一些值并输出,看看我们的表现如何:

因此,当我们运行我们的程序时,它会询问我们要给它什么inputType。让我们给它一个华氏值。现在让我们说我们想要得到一个摄氏值作为输出。让我们看看32华氏度对应的摄氏值是多少。我们看到输出结果是032华氏度是0摄氏度,这是一个好迹象。让我们尝试一些更极端的情况。如果我们试图将摄氏度转换为摄氏度,我们得到的值与下面的截图中显示的值相同,这是我们所期望的:

让我们看看1开尔文度对应的华氏值是多少:

好消息是,这也是前面截图中的预期值。我们使用函数使一个本来非常复杂和难以阅读的程序变得更加可管理。我们在这里编写的程序有些复杂。它进行了一些数学和多功能语句,所以如果你第一次没有完全理解,我鼓励你回去检查是什么让你困惑。还有其他方法来解决这个问题,如果你有灵感,我鼓励你去探索一下。

高级 Java 函数

在这一部分,我希望你深入了解 Java 方法,并学习一些关于编程语言如何思考和操作信息的非常有价值的东西。为了帮助我们做到这一点,我想进行一种实验,并且为了开始这个实验,我写了一个非常基本的 Java 程序:

package advancedmethods; 

public class AdvancedMethods { 
    public static void main(String[] args) { 
        int x = 5; 
        magic(x); 
        System.out.println("main: " + x); 
    } 
    public static void magic(int input) 
    { 
        input += 10; 
    } 
} 

在这个 Java 程序的核心是magic方法,它是在main方法之后用户自定义的。当我们遇到一个新的 Java 方法时,有三件事情我们应该注意:

  1. 首先,我们应该问,“它的输入值是什么?”在我们的magic方法中,它只期望一个整数作为输入。

  2. 然后,我们可能想问,“这个方法返回什么?”。在我们的情况下,该方法标记为返回void。Void 方法实际上根本不返回任何值;它们只是执行它们的代码并完成。您会注意到,当我们在程序的主要部分引用magic时,我们并没有尝试将其返回值存储在任何位置。这是因为当然没有返回值可以存储。

  3. 然后,关于我们的方法要注意的第三件事是“它做什么?”。在我们的magic方法的情况下,我们只是取得我们作为input得到的值,并将该值增加10

我想现在要求你做的是花一分钟时间,仔细看看这个程序,并尝试弄清楚当我们到达这个println语句时,程序的输出将是什么。这里的挑战性问题是当我们运行magic(x)代码行并调用我们的magic方法时,变量x的值会发生什么变化?当我们将其作为值传递给magic方法时,变量x是否保持不变,或者变量x是否被magic方法中的输入代码行修改,以至于我们打印出15而不是5的值?

要回答这个问题,我们只需要运行我们的程序,如果我们这样做,我们将看到我们得到了5的值,这让我们知道运行magic方法并没有修改主方法中变量x的值:

实际上,如果我们根本不运行magic方法,我们将得到相同的输出。那么这告诉我们什么?这为我们提供了一个非常重要的见解,即 Java 如何处理方法输入。要完全理解这里发生了什么,我们需要更深入地了解 Java 变量的操作。

操作 java 变量

以下是我们的变量x存储的信息的表示,即我们 Java 程序的main方法中的变量:

您会注意到这个变量有三个核心组件;让我们快速浏览一下:

  • 在左侧,我放置了这个变量的名称,这是我们在范围内引用它所使用的关键字,以及一个内存位置。我们的变量指向一个内存位置,在这个内存位置中,我们存储变量的值。

  • 我们可以将名称和内存位置视为非常静态的;在我们程序执行过程中,这个单独的变量标识符不会真正改变。然而,我们可以自由地更改变量引用的内存位置中存储的值。

那么这为什么重要呢?好吧,在我们的程序过程中,我们将不得不将存储在变量x中的信息转换为我们的magic方法试图使用的变量输入中存储的信息。如果我们仔细看看变量的设置方式,我们很快就会发现有两种可能的方法来做到这一点:

  1. 首先,我们可以简单地创建一个名为input的全新变量,具有其自己独特的内存位置,然后简单地将我们在x引用的内存位置中找到的相同值放置在该内存位置中的值中:

当我们将变量x传递给一个方法时,这是 Java 用来创建变量input的技术,我们可以说 Java 通过值传递了我们的变量x。这是因为只有值在创建新变量时被保留。

  1. 另一个选项是我们创建一个全新的变量input,但是我们不仅仅是将变量x的值复制到变量input,我们可以使input引用与x相同的内存位置。这将被称为通过引用传递变量x。在这种情况下,因为xinput都共享一个内存位置来存储它们的值,修改变量input的值也会修改变量x的值。

因此,根据您刚刚了解的关于 Java 变量的知识,并考虑到在magic(x)代码行上执行magic方法不会修改变量x的值,我们可以正确地得出结论,Java 选择通过值而不是通过引用将变量传递给其方法。

然而,这并不是故事的结束,或者说,这个事实可能对我们来说并不立即显而易见。如果我们重写我们的程序,使我们的magic方法接受字符输入、布尔输入或任何其他原始类型,我们将看到与我们已经看到的相同的行为。即使在magic方法的范围内修改此input变量的值,也不会修改main方法的范围内的变量x的值。所以,事情并不总是那么简单。

在程序中使用变量

为了看到这一点,让我们创建一个全新的方法,在它的声明中,我们将它与我们现有的magic方法相同。但是,我们将以整数数组的形式提供它作为输入:

package advancedmethods;
public class AdvancedMethods {
    public static void main(String[] args) {
        int[] x = 5;
        magic(x);
        System.out.println("main: " + x);
    }

    public static void magic(int input)
    {
        input += 10;
    }
    public static void magic(int[] input)
    {
        input += 10;
    }
}

记住,我们的数组将被命名为一个单一的变量,所以我们需要做的就是让 Java 知道我们想要将一个数组传递给函数,通知它给定的变量是某种类型的数组。您还会注意到,我们现在在程序中有两个名为magic的方法。这被称为方法重载,只要 Java 有办法区分这些方法,这样做就是完全合法的。在这种情况下,Java 可以区分这些方法,因为这两个方法将被赋予不同的对象作为输入。

如果给magic调用的输入是单个整数,则我们的magic方法之一将执行,如果给方法的输入是整数数组,则我们的新magic方法将执行。现在,让我们编写一个快速的for循环,这样我们的新magic方法将将输入数组中的每个整数的值增加10

public static void magic(int[] input) 
{ 
    for(int i = 0; i < input.length; i++) 
    input[i] += 10; 
} 

这与我们最初编写的magic方法非常相似,只是它不是操作单个整数,而是操作任意数量的整数。然而,当我们修改我们的main方法以利用magic方法的新实现时,可能会发生一些奇怪的事情。为了实现这一点,我们需要对我们的程序进行一些快速修改。

让我们将变量x从整数更改为整数数组,这样我们的程序将知道如何利用新编写的magic方法,当我们给定整数数组作为输入时,它将运行:

package advancedmethods; 

import java.util.*; 

public class AdvancedMethods { 
    public static void main(String[] args) { 
        int[] x = {5,4,3,2,1}; 
        magic(x); 
        System.out.println("main: " + Arrays.toString(x)); 
    } 
    public static void magic(int input) 
    { 
        input += 10; 
    } 
    public static void magic(int[] input) 
    { 
        for(int i = 0; i < input.length; i++) 
        input[i] += 10; 
    } 
} 

我们还需要修改我们的println语句,以利用Arrays.toString来正确显示x数组中存储的值。我们将导入java.util,以便 Java 知道Arrays库:

import java.util.*; 

public class AdvancedMethods { 
    public static void main(String[] args) { 
        int[] x = {5,4,3,2,1}; 
        magic(x); 
        System.out.println("main: " + Arrays.toString(x)); 
    } 

现在是时候问自己另一个问题了:当我们在整数数组上运行magic函数时,我们是否会看到与我们在单个整数值上运行magic函数时看到的相同结果,即原始类型?要回答这个问题,我们只需要运行我们的程序,我们很快就会看到,存储在x数组中的输出或最终值与我们最初分配给x数组的值不同:

这让我们知道我们的magic方法确实修改了这些值。这有点奇怪。为什么我们的magic方法会根据我们给它的是单个原始类型还是原始类型数组而有不同的操作?为了回答这个问题,让我们看看当变量x被声明为整数数组而不是我们之前的单个整数时会发生什么:

请注意,x作为一个整数数组,而不是单个原始类型,仍然具有名称和内存位置来标识它以及它可以存在的位置;但是,它的值字段看起来与以前大不相同。当x只是一个整数时,我们可以简单地将一个显式整数存储在x的值字段中,但是作为数组,x意味着能够引用许多不同的值;这就是它成为数据结构的原因。为了实现这一点,数组-实际上每个比原始类型更复杂的元素-指向内存中的一个位置,而不是单个显式值。对于数组,我们只需要指向内存中数组的 0 索引。然后,通过从该索引开始,我们可以存储许多不同的值,我们的变量x知道如何访问。那么这为什么重要呢?

理解传递参数

好吧,让我们看看当我们按值传递x到方法时会发生什么。我们知道,当我们按值传递一个变量时,我们告诉 Java 在方法的上下文中创建一个新变量,该变量将具有自己独特的名称和内存位置:

然而,在我们的例子中,这个新变量-input-获取了旧变量的值作为自己的值。当我们处理原始类型时,这些值是完全独立的,但现在inputx都具有相同内存位置的值。因此,修改输入的值不会改变x的值,但修改输入指向的内存位置仍会改变x查看时的内存位置。

在方法的上下文中,如果我们明确引用一个输入变量,然后修改该变量,我们将只修改函数上下文中的变量,就像我们在第一个magic方法中所做的那样。但是,如果我们必须采取额外的步骤来访问我们正在修改的值,就像我们在声明数组的索引时必须做的那样,那么我们可能必须通过内存位置或引用来修改它。在这种情况下,我们可能会影响为我们函数变量提供值的变量:

package advancedmethods; 

import java.util.*; 

public class AdvancedMethods { 
    public static void main(String[] args) { 
        int[] x = {5,4,3,2,1}; 
        magic(x); 
        System.out.println("main: " + Arrays.toString(x)); 
    } 
    public static void magic(int input) 
    { 
        input += 10; 
    } 
    public static void magic(int[] input) 
    { 
        input = new int[] {2,2,2,2,2}; 
    } 
} 

如果我们的接受数组的magic函数尝试将我们的整数数组的值设置为全新的整数值集合,并具有全新的起始内存位置,我们会发现当我们在其上运行此函数时,我们将不再修改x的值:

这是因为创建一个新的整数数组导致我们明确改变了输入的值。在这行代码之后,inputx不再共享值。非常感谢您的时间。希望您学到了一些东西。

总结

您还在吗?如果是的,恭喜。我们从一些基本的 Java 函数开始,比如方法,然后继续理解高级 Java 函数。我们刚刚讨论了一些复杂的东西。随着您成为更有经验的程序员,您将开始内化这些概念,当您编写日常代码时,您不必明确考虑它们。不过,现在有一些逻辑快捷方式可以帮助我们避免太多的困扰。

在下一章中,您将详细了解使用面向对象的 Java 程序进行建模。

第六章:用面向对象的 Java 建模

在本章中,你将学习如何在 Java 中创建类和对象。面向对象编程使我们能够向计算机和自己解释高度复杂的系统。此外,关于对象如何相互配合、它们可以有哪些关系以及我们可以如何使用对象来使我们的程序更容易编写,还有很多要学习的关于面向对象编程的内容。我们还将讨论创建自定义类、成员变量和成员函数的主题。最后,我们将研究分配给我们自定义类的一个非常特殊的成员,即构造函数,以及构造函数的类型。

在本章中,我们将涵盖以下主题:

  • 创建类和对象

  • 创建自定义类

  • 创建成员变量

  • 创建成员函数

  • 创建构造函数

  • 构造函数的类型

创建类和对象

在这一部分,你将迈出学习 Java 面向对象编程的第一步。所以我想问的第一个问题是,“什么是面向对象编程?”嗯,在高层次上,面向对象编程是创建对象的过程,这些对象是独特的、相互独立的代码和逻辑实体,但它们之间可以有复杂的关系。

当我们编写面向对象的代码时,我们开始将代码看作一组物理部件或对象。Java 本质上是一种面向对象的语言。因此,如果你一直在学习 Java,至少你已经在使用对象而没有意识到。

要看到面向对象编程的威力,看一下下面的程序(GettingObjectOriented.java):

package gettingobjectoriented; 

import java.util.*; 

public class GettingObjectOriented { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 

        System.out.println(reader.next()); 
    } 
} 

这个程序是一个非常基本的输入/输出程序,如果你一直在学习 Java,你可能已经写过这种程序。在这个程序中,我们使用了一个名为Scanner的对象,我们称之为reader,你会注意到我们在两行上使用了reader:在一行上,我们声明并初始化了reader,在另一行上,我们调用了readernext()函数来获取一些用户输入。

我希望你注意到这两行代码之间的关系的重要之处是,当我们声明reader时,我们为它提供了除了简单地创建一个新的Scanner对象的命令之外的一些额外信息。这很有趣,因为当我们后来使用readernext()函数时,我们不需要重新告诉它应该从哪个流中读取;相反,这些信息会被reader对象自动存储和调用。

这就是面向对象编程的美妙之处:我们创建的实体或对象可以被构造成这样一种方式,不仅它们知道如何处理给予它们的信息并为我们提供额外的功能,而且它们也知道要询问什么信息来执行它们以后的任务。

让我们确保我们的术语准确。首先,让我们分析我们代码中的new Scanner(System.in)部分。这个命令告诉 Java 为我们的程序创建一个新对象,一个新的Scanner对象。这个对象有它所在的位置和内存,这个位置由reader变量引用。我们可以创建多个变量,它们都指向同一个Scanner对象;然而,在这个简单程序的上下文中,reader是我们指向对象内存位置的唯一入口点。因此,我们通常可以通过它的变量名来简单地引用一个对象。

最后,不同的对象以不同的方式运行。我们可以创建多个Scanner对象;它们在内存中的位置可能不同,但它们会共享类似的功能。声明对象具有什么功能以及该功能如何运行的代码和逻辑称为对象的类。在这种情况下,我们正在创建一个Scanner类的对象,并用reader变量指向它。

这一切都很好,我们可以简单地使用 Java 提供的默认标准库创建许多程序;然而,为了真正打开大门,我们需要能够创建自定义的类。让我们开始并创建一个。

创建自定义类

现在,我们可以在我们已经在工作的文件中创建一个新的类;然而,类声明代码与像执行的main()方法之类的逻辑上是不同的,其中代码行是按顺序依次执行的。相反,我们要创建的类将更多地作为代码行的参考,比如Scanner reader = new Scanner(System.in);这行代码。通常,在面向对象的语言中,像 Java 这样的高级面向对象的语言,我们只需将我们创建的每一个新类放在自己单独的文件中。

要为我们的类创建一个新的 Java 文件,只需右键单击屏幕左侧的包名,即gettingobjectoriented。然后,选择新建,然后选择 Java 类。之后,我们只需提示给它一个名称。

在这种情况下,我们将创建一个类来提供和存储有关一个人的一些基本信息。我们将称之为Person类,它创建人物对象:

当我们按下“完成”时,NetBeans 非常方便,为我们设置了一些非常基本的代码行。它声明这个类在我们的本地包中。这意味着当我们从我们的main()方法中引用它时,我们不必像引用标准库那样导入这个类。NetBeans 很友好地为我们创建了类声明。这只是一行代码,让 Java 编译器知道我们将要声明一个新的类,如下面的屏幕截图所示:

package gettingobjectoriented;
public class Person {
}

现在,我们将忽略public关键字,但知道它在这里是非常必要的。class关键字让我们知道我们将要声明一个类,然后就像我们创建并需要在将来引用的一切一样,我们给类一个名称或一个唯一的关键字。

现在是时候编写代码来设置我们的Person类了。请记住,我们在这里所做的是教会程序的未来部分如何创建Person对象或Person类的实例。因此,我们在这里编写的代码将与我们在一个简单地执行从头到尾的方法中所写的代码非常不同。

我们在类声明中放置的信息将属于这两类之一:

  • 第一类是我们告诉 Java Person类应该能够存储什么信息

  • 第二类是我们教 JavaPerson对象应该暴露什么功能

创建成员变量

让我们从第一类开始。让我们告诉 Java 我们想在Person中存储什么信息:

package gettingobjectoriented; 

public class Person { 
    public String firstName; 
    public String lastName; 
} 

告诉 Java 要存储的信息很像在任何其他代码中声明变量。在这里,我们给Person类两个成员变量;这些是我们可以在任何Person对象中访问的信息。

在类声明中,几乎我们声明的每一样东西都需要给予保护级别。当我们成为更高级的 Java 用户时,我们将开始使用不同的保护级别,但现在,我们只是简单地声明一切为“public”。

因此,正如我们在这里设置的那样,每个Person对象都有firstNamelastName。请记住,这些成员变量对于Person对象的每个实例都是唯一的,因此不同的人不一定共享名字和姓氏。

为了让事情变得更有趣,让我们也给人们分配生日。我们需要导入java.util,因为我们将使用另一个类Calendar类:

package gettingobjectoriented; 
import java.util.*; 
public class Person { 
    public String firstName; 
    public String lastName; 
    public Calendar birthday; 
} 

日历基本上是点和时间或日期,具有大量功能包装在其中。很酷的是Calendar是一个独立的类。因此,我们在Person类中放置了一个类;String也是一个类,但 Java 认为它有点特殊。

现在,让我们回到GettingObjectOriented.java文件中的main()方法,看看创建一个全新的人是什么样子。现在,我们将保留这行代码,以便将其用作模板。我们想要创建我们的Person类的一个新实例或创建一个新的Person对象。为此,我们首先要告诉 Java 我们想要创建什么类型的对象。

因为我们在使用的包中声明了Person类,Java 现在将理解Person关键字。然后,我们需要给我们将分配新人的变量一个名字;让我们将这个人命名为john。创建一个新人就像创建一个新的Scanner对象一样简单。我们使用new关键字让 Java 知道我们正在创建一些全新的尚不存在的东西,然后要求它创建一个人:

package gettingobjectoriented; 

import java.util.*; 

public class GettingObjectOriented { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        Person john = new Person(); 
        System.out.println(reader.next()); 
    } 
} 

在这里,Person john = new Person ();将导致变量john指向的人,我们将简单地认为是一个人 John,出现。现在john已经具有一些基本功能,因为我们已经为Person类声明了一些成员变量,因此即使我们对Person类的基本声明也给了 John 一些我们可以使用的成员变量。

例如,johnfirstName,我们可以使用点(.)运算符作为变量进行访问,并且我们可以继续为这个变量分配一个值。我们也可以用同样的方法处理 John 的姓和当然是他的生日:

package gettingobjectoriented;
import java.util.*;
public class GettingObjectOriented {
    public static void main(String[] args) {
        Scanner reader = new Scanner(System.in);
        Person john = new Person();
        john.firstName = "John";
        john.lastName = "Doe";
        john.birthday = 
        System.out.println(reader.next());
    }
}

现在,我已经提到birthday在我们到达这一点时会与firstNamelastName有些不同。虽然字符串在 Java 中在技术上是类,但 Java 也赋予它们能够被分配给显式值或字符串显式的特权。当然,日历没有这种独特的特权,因此我们需要创建一个新的Calendar对象放在我们的对象中,也就是john

现在,Calendar是我们可以分配实例的类之一;但是,当我们想要创建一个全新的实例时,我们需要创建一个更具体的也是日历的东西。因此,对于这个实例,我们将使用GregorianCalendar。然后,让我们将birthday分配给john,比如1988,1,5。然后,为了查看一切是否按预期分配,只需打印出 John 的名和姓。

我们运行以下程序时:

package gettingobjectoriented;
import java.util.*;
public class GettingObjectOriented {
    public static void main(String[] args) {
        Scanner reader = new Scanner(System.in);
        Person john = new Person();
        john.firstName = "John";
        john.lastName = "Doe";
        john.birthday = new GregorianCalendar(1988,1,5);
        System.out.println(john.firstName + john.lastName);
    }
}

我们看到John Doe并没有真正格式化,但是按预期打印到屏幕上:

我们已经成功地将信息存储在我们的john对象中。如果我们愿意,我们可以创建一个全新的人“Jane”,她将拥有自己的firstNamelastNamebirthday;她的成员变量完全独立于 John 的。

创建成员函数

让我们回到我们的Person类,也就是Person.java文件,并为人们提供更多功能。因此,面向对象的 Java 的美妙之处在于,我们已经开始将我们的Person类的实例视为物理对象。这使得预期将会问到他们的问题变得更容易。

例如,当我遇到一个新的人时,我大多数情况下要么想知道他们的名字,要么想知道他们的全名。所以,如果我们的人存储了一个名为fullName的字符串,人们可以直接询问而不必单独获取他们的名字和姓氏,这不是很好吗?

当然,简单地添加另一个成员变量是不方便的,因为创建Person的新实例的人需要设置fullName。而且,如果人的名字、姓氏或全名发生变化,他们的fullNamefirstNamelastName变量可能不会正确匹配。但是,如果我们提供一个成员方法而不是成员变量呢?

当我们在类的上下文中创建方法时,我们可以访问类的成员变量。如果我们想要修改它们,或者像我们刚刚做的那样,我们可以简单地利用它们的值,比如返回这个人动态构造的全名。

package gettingobjectoriented; 
import java.util.*; 
public class Person { 
    public String firstName; 
    public String lastName; 
    public Calendar birthday; 
    public String fullName() 
    { 
         return firstName + " " + lastName; 
    } 
} 

我预计这个人会被问到另一个问题,那就是你多大了?这将很像我们刚刚写的方法,只有一个例外。为了知道这个人多大了,这个人需要知道今天的日期,因为这不是这个人已经存储的信息。

为了做到这一点,我们将要求人们在调用这个方法时传递这些信息,然后我们将简单地返回今天年份与这个人的生日年份之间的差异。

现在,从日历中获取年份的语法有点奇怪,但我认为我们应该能够理解。我们只需使用get方法,它有许多用途,然后我们需要告诉方法我们想从中获取什么,我们想从中获取一个日历年(Calendar.YEAR)。所以,让我们确保保存这个文件,跳转到我们的main方法,并利用我们刚刚添加到Person实例的新方法之一:

package gettingobjectoriented;
import java.util.*;
public class Person {
    public String firstName;
    public String lastName;
    public Calendar birthday;
    public String fullName()
    {
         return firstName + " " + lastName;
    }
    public int age(Calendar today)
    {
         return today.get(Calendar.YEAR) - birthday.get(Calendar.YEAR);
    }
}

所以,我们设置了john。他有一个生日。让我们在这里的println语句中问 John 他多大了。为了做到这一点,我们只需调用 John 的age方法,并创建一个新的Calendar对象传递进去。我认为新的GregorianCalendar实例将默认设置为当前日期和时间。

如果我们运行以下程序:

package gettingobjectoriented;
import java.util.*;
public class GettingObjectOriented {
    public static void main(String[] args) {
        Scanner reader = new Scanner(System.in);
        Person john = new Person();
        john.firstName = "John";
        john.lastName = "Doe";
        john.birthday = new GregorianCalendar(1988,1,5);
        System.out.println(john.age(new GregorianCalendar()));
    }
}

我们看到 John 今年29岁:

这就是我们的基本介绍了。这是我们对面向对象的 Java 的基本介绍,但最终都会归结为你刚学到的基础知识。

创建构造函数

在这一部分,你将学习到我们可以分配给自定义类的一个非常特殊的成员,那就是构造函数。首先,让我们看一下下面的代码:

package gettingobjectoriented; 

import java.util.*; 

public class GettingObjectOriented { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 
        Person john = new Person(); 
        john.firstName = "John"; 
        john.lastName = "Doe"; 
        john.birthday = new GregorianCalendar(1988,1,5); 
        System.out.println( 
            "Hello my name is " +            
            john.fullName() + 
            ". I am " + 
            john.age(new GregorianCalendar()) + 
            " years old."); 
    } 
} 

这个程序创建了我们自定义类Person的一个实例,并立即为Person的成员变量firstNamelastNamebirthday赋值。然后,我们利用Person的一些成员函数打印出我们刚刚分配的一些信息。

虽然这是一个不错的程序,但很容易看到即使是这样一个简单的程序,也可能出现错误。例如,如果我忘记了或者根本没有意识到birthdayPerson的成员变量之一会怎么样?如果我不立即为一个人分配生日,然后尝试使用age()成员方法,就像下面的代码块中所示的那样:

package gettingobjectoriented; 

import java.util.*; 

public class GettingObjectOriented { 
    public static void main(String[] args) { 
        Person john = new Person(); 
        john.firstName = "John"; 
        john.lastName = "Doe"; 
        //john.birthday = new GregorianCalendar(1988,1,5); 
        System.out.println( 
        "Hello my name is " + 
        john.fullName() + 
        ". I am " + 
        john.age(new GregorianCalendar()) + 
        " years old."); 
    } 
} 

当程序尝试访问尚未设置任何内容的生日变量时,我们的程序将崩溃,如下面的截图所示:

对于程序员来说,这是一个非常合理的错误,既不知道他们应该将这个成员变量设置为一个值,也假设这个成员变量会有一个值,因为什么样的人没有生日呢?幸运的是,我们有一个系统,可以在允许用户创建对象实例之前要求用户提供信息。因此,让我们进入声明Person类的代码,并设置这个类,以便只有在一开始就提供了所有必要的信息时才能创建一个人。为此,我们将使用构造函数。

构造函数声明看起来很像普通方法声明,除了一点。普通方法会有一个返回值,甚至如果它不打算返回任何东西,也会有一个 null 值;构造函数甚至没有那个。此外,构造函数方法的名称与我们分配给类的名称相同;然而,就像普通方法一样,我们可以给构造函数传入参数。

首先,让我们假设所有人都有“名”、“姓”和“生日”;否则,他们根本就不应该存在。当我们创建Person类的新实例并且Person类已经定义了构造函数时,我们将始终使用Person构造函数创建类的实例:

package gettingobjectoriented; 

import java.util.*; 

public class Person { 
    public String firstName; 
    public String lastName; 
    public Calendar birthday; 

    public Person(String firstName, String lastName, Calendar birthday) 
    { 

 } 

    public String fullName() 
    { 
         return firstName + " " + lastName; 
    } 

    public int age(Calendar today) 
    { 
         return today.get(Calendar.YEAR) - birthday.get(Calendar.YEAR); 
    } 
} 

如果我们保存了对Person类声明的这个更新,然后回到我们程序的main方法,我们将得到一个编译器错误,如下面的截图所示:

这是因为我们修改了Person类,要求我们使用新创建的构造函数。这个构造函数接受三个输入值:一个字符串,一个字符串和一个日历。因此,我们不会在这三行代码中修改Person的成员变量,而是将这三个变量作为参数传递给我们的构造函数方法:

package gettingobjectoriented; 

import java.util.*; 

public class GettingObjectOriented { 
    public static void main(String[] args) { 
        Person john = new Person("John", "Doe", newGregorianCalendar(1988,1,5)); 

        System.out.println( 
        "Hello my name is " + john.fullName() + ". I am " + john.age(new 
        GregorianCalendar()) + 
        " years old."); 
    } 
} 

现在,就我们的程序中的main方法而言,程序的语法再次是有效的。当然,如果我们运行这个程序,我们将遇到一些麻烦,因为虽然我们将这些参数传递给Person构造函数,但我们还没有对它们做任何处理。

现在,这里的工作应该是我们的Person构造函数的工作,而不是我们 Java 程序中的main方法,将这些参数转换为Person的成员变量的值。所以,让我们这样做。让我们将Person类的firstName更改,或者说将其值设置为传递给这个函数的变量:

package gettingobjectoriented;
import java.util.*;
public class Person {
    String firstName;
    String lastName;
    Calendar birthday;
    public Person(String firstName, String lastName, Calendar birthday)
    {
         firstName = firstName;
    }
    public String fullName()
    {
         return firstName + " " + lastName;
    }

    public int age(Calendar today)
    {
         return today.get(Calendar.YEAR) - birthday.get(Calendar.YEAR);
    }
}

现在,这是一个技术上正确的语法;它将做我们想要做的事情。

firstName = firstName这行代码真的很奇怪,如果你仔细阅读它,它是相当模糊的。毕竟,在每个实例中,我们在谈论哪个firstName变量?我们是在谈论Person.firstName,这个类的成员变量,还是在谈论作为构造函数方法参数传递的firstName?为了消除这种歧义,我们可以做一些事情。

首先,我们可以简单地更改我们分配给方法参数的名称,使其不与本地成员名称相同;然而,有时明确要求firstName是有意义的。对于将要使用构造函数的人来说,这可能更容易。当我们需要明确告诉我们的程序,我们正在使用Person类的成员变量之一时,我们应该正确地为其提供路径。this关键字将允许我们在程序运行时访问我们当前操作的类,或者说它的对象实例。因此,this.firstName将始终引用成员变量,而不是作为参数传递的变量。现在我们有了语法,我们可以快速地将参数值分配给我们的成员变量的值:

现在,当我们保存这个文件并返回到我们的main方法——也就是GettingObjectOriented.java——并运行我们的程序时,我们将得到原始输出,显示我们的Person构造函数已经正确地将这些输入值映射到我们Person对象中存储的值:

所以这很酷。我们修改了我们的Person类,使得程序员更难犯一个明显的错误并在它们注定失败时调用这些方法。如果程序员在创建我们的人之后修改了成员变量中的一个,他们仍然可能遇到麻烦。

然而,如果我们选择的话,有一个系统可以保护我们的类,使其成员不能在没有经过适当协议的情况下被修改。假设我们想要更改我们的Person类,以便这些成员只在构造函数调用时被修改一次。如果你记得的话,我们一直在给我们的类的所有成员打上public保护标签。被标记为public的东西基本上可以被我们程序中任何有权访问其容器的部分随时查看。

然而,我们可以使用一些其他不同的保护标签。如果我们将所有成员变量标记为private,那么它们只能在其当前类的上下文中查看。因此,我们仍然可以在我们的Person构造函数和我们的fullNameage方法中使用成员变量,但是当我们尝试在实际类声明之外访问lastName时,它将是无效的:

package gettingobjectoriented; 

import java.util.*; 

public class Person { 
    private String firstName; 
    private String lastName; 
    private Calendar birthday; 

我们可以将成员标记为private,然后创建公共方法在适当的时候修改它们的值。通过这样做,我们将保护我们的对象免受无效值的影响。

构造函数的类型

现在,让我们回到谈论构造函数,然后结束。与普通方法一样,我们可以重写构造函数,并为程序员提供多个选择。

例如,假设在我们的程序中有时我们想要创建刚出生的新人。在这种情况下,我们可能会通过简单地将firstNamelastName传递给我们的构造函数,然后将birthday设置为new Gregorian Calendar来构造一个人,这将默认为今天的日期:

package gettingobjectoriented; 

import java.util.*; 

public class Person { 
    private String firstName; 
    private String lastName; 
    private Calendar birthday; 
    public Person(String firstName, String lastName) 
    { 
         this.firstName = firstName; 
         this.lastName = lastName; 
         this.birthday = new GregorianCalendar(); 
    } 

    public Person(String firstName, String lastName, Calendar 
    birthday) 
    { 
         this.firstName = firstName; 
         this.lastName = lastName; 
         this.birthday = birthday; 
    } 

如果我们想在我们的程序中使用这个构造函数,我们只需调用只有两个字符串参数的构造函数。这将映射到我们在这里声明的新创建的构造函数。

考虑以下程序:

package gettingobjectoriented; 

import java.util.*; 

public class GettingObjectOriented { 
    public static void main(String[] args) { 
            Person john = new Person("John", "Doe"); 

            System.out.println( 
                    "Hello my name is " +            
                    john.fullName() + 
                    ". I am " + 
                    john.age(new GregorianCalendar()) + 
                    " years old."); 
    } 
} 

当我们运行它时,由于出生日期已设置为当前日期和时间,我们将看到John Doe现在是0岁,如下面的截图所示:

最后,我们可以让某人选择使用我们的构造函数之一,或者只需创建一个不做任何事情的类的实例,只需声明一个空的构造函数。然后,语法看起来就像我们之前参与的 John 的创建一样:

public Person() 
{ 

} 

一般来说,我们不想这样做。如果我们有一个空的或默认的构造函数,我们想要做的是为我们的成员变量分配默认值,这样至少,我们仍然不会破坏我们的程序。因此,我们的默认构造函数可能会将空字符串和今天的日期分配给我们的firstNamelastNamebirthday字段:

public Person() 
    { 
        firstName = ""; 
        lastName = ""; 
        birthday = new GregorianCalendar(); 
    } 

然后,即使我们的程序员在创建 John 的字段后没有正确地为它们分配值,这些字段中仍然会有一些有效的值,以保护我们免受在运行以下程序时实际抛出错误的影响:

package gettingobjectoriented; 

import java.util.*; 

public class GettingObjectOriented { 
    public static void main(String[] args) { 
            Person john = new Person(); 

            System.out.println( 
                    "Hello my name is " +            
                    john.fullName() + 
                    ". I am " + 
                    john.age(new GregorianCalendar()) + 
                    " years old."); 
    } 
} 

以下是前面代码的输出:

这就是构造函数的要点,它是另一个帮助我们保护和使我们已经编写的代码更加健壮的工具。

总结

在本章中,我们看到了如何创建类和对象,以及如何创建成员变量和函数,这将使我们的代码变得不那么复杂。您还学习了关于创建分配给类的构造函数和构造函数类型的知识。

第七章:更多面向对象的 Java

在本章中,我们将通过创建超类和子类,理解它们之间的“is-a”关系,使用覆盖、数据结构、抽象方法和受保护方法等概念,来探讨 Java 中的继承。

我们将详细介绍以下概念:

  • 继承

  • 抽象

继承

与其从一个高层描述开始,我认为最好的方法是我们直接解决一个问题。

为了让我们开始,我创建了一个基本的 Java 程序,我们可以从给定的代码文件中访问。在这个程序中,我们声明了两个 Java 类:一个Book类和一个Poem类。BookPoem类都存储了许多属性;例如,Book 可以有一个标题,一个作者,一个出版商和一个流派。它将所有这些属性作为构造函数输入,并提供一个public方法;我们可以在我们的主程序中使用Print方法来打印出我们创建的任何书籍的信息。

诗歌方法做的事情非常相似。它有一些属性和一个Print方法,我们通过它的构造函数设置它的属性。我匆匆忙忙地写了一个利用BookPoem类的主函数。这个函数创建了一本新书和一首新诗,然后将它们打印出来:

package inheritance;
public class Inheritance {
    public static void main(String[] args) {
        Book a = new Book(
                "The Lord Of The Rings", 
                "J.R.R. Tolkein",
                "George Allen and Unwin", 
                "Fantasy");
        Poem b = new Poem(
                "The Iliad",
                "Homer",
                "Dactylic Hexameter");

        a.Print();
        b.Print();
    }
}

前面的程序运行良好,但比必要的要复杂得多。

如果我们一起看看我们的BookPoem类,并只看它们的成员变量,我们会发现BookPoem都共享两个成员变量,即titleauthor

他们对成员变量所采取的操作,即将它们打印到屏幕上,都是以非常相似的方式在两个类中执行和实现的:

BookPoem从一个共同的类继承是一个好迹象。当我们将书籍和诗歌视为它们所代表的物体时,我们很容易看到这一点。我们可以说书籍和诗歌都是文学形式。

创建一个超类

一旦我们得出结论,即书籍和诗歌共享某些基本属性,所有文学作品的属性,我们就可以开始将这些类分解为组成部分。例如,我们的Book类有两个真实变量。它有一个title变量和一个author变量,这些是我们与所有文学作品相关联的属性。它还有一个publisher变量和一个genre变量,这些可能不仅仅是书籍独有的,我们也不一定认为所有形式的文学作品都具有这些属性。那么我们如何利用这些知识呢?嗯,我们可以构建我们的BookPoem类,使它们在基本层面上共享它们作为文学作品的本质。但是,要实现这一点,我们首先需要教会我们的程序什么是一部文学作品。以下是一个逐步的过程:

  1. 我们将创建一个全新的类,并将其命名为Literature

  2. 我们将为这个类分配我们迄今为止声明的文学作品共享的属性。在我们的情况下,书籍和诗歌已经被声明为作品,具有共享的标题和作者。将所有文学作品都具有标题和作者是有一定逻辑意义的:

package inheritance;
public class Literature {
    protected String title;
    protected String author;
  1. 从这里开始,我们将像处理任何其他类一样完善我们的Literature类。我们将给它一个构造函数;在这种情况下,我们的构造函数将接受两个变量:titleauthor。然后,我们将它们分配给字段,就像我们对PoemBook类所做的那样:
package inheritance;
public class Literature {
  protected String title;
  protected String author;

  public Literature(String title, String author)
  {
     this.title = title;
     this.author = author;
   }
  1. 在这个过程中,让我们给Literature一个类似的Print方法,就像我们为BookPoem类分配的那样:
public void Print()
{
   System.out.println(title);
   System.out.println("\tWritten By: " + author);
 }

现在,如果我们愿意,我们可以去我们的main方法,并声明一个Literature类的对象,但这不是重点。这不是我们创建Literature类的原因。相反,我们的目标是利用这个Literature类作为一个基础,我们将在其上声明更多特定类型的文学作品,比如诗歌或书籍。为了利用我们的Literature类,让我们看看它如何适用于现有的Poem类。

是一个关系

我们的Literature类包含了管理文学作品标题和作者的声明和所有功能。如果我们让 Java 知道PoemLiterature之间存在继承关系,我们应该能够删除以下Poem类的标题和作者的所有真实引用:

package inheritance;
public class Poem extends Literature{
    private String title;
    private String author;
    private String style;

首先,让我们谈谈我们修改过的Poem类的声明。当我们说一个类扩展另一个类时,我们是在说它们之间存在一个是关系,以至于我可以逻辑地说出这样的陈述:“一首诗是一种文学作品。”更多的是 Java 术语,我们是在说Poem子类扩展或继承自Literature类。这意味着当我们创建一个Poem对象时,它将拥有它扩展的类的所有成员和功能:

package inheritance;
public class Poem extends Literature {
    private String style;

    public Poem(String title, String author, String style)

在我们的情况下,其中两个成员是titleauthorLiterature类声明了这些成员,并且在整个类的功能中很好地管理它们。因此,我们可以从我们的Poem类中删除这些成员,我们仍然可以在Poem类的方法中访问它们。这是因为Poem类只是从Literature继承了它的声明。但是,我们需要进行轻微修改,以使Poem类按预期工作。当我们构造从另一个类继承的类的对象时,默认情况下,子类的构造函数将首先调用超类的构造函数:

package inheritance;
public class Literature {
    protected String title;
    protected String author;

    public Literature(String title, String author)
    {
         this.title = title;
         this.author = author;
    }

这让 Java 感到困惑,因为我们现在设置的是Poem构造函数接受三个变量作为输入,而Literature构造函数只期望两个。为了解决这个问题,在Poem构造函数中显式调用Literature构造函数,使用以下步骤:

  1. 当我们在子类中时,我们可以使用super关键字调用我们超类的方法。因此,在这种情况下,我们将通过简单地调用super构造函数,或者Literature构造函数来开始我们的Poem构造函数,并向它传递我们希望它知道的属性:
public Poem(String title, String author, String style)
{
     super(title, author);
     this.style = style;
 }
  1. 我们可以在我们的Print方法中做类似的事情,因为我们的Literature类,我们的超类,已经知道如何打印标题和作者。Poem类没有实现这个功能是没有理由的:
 public void Print()
 {
      super.Print();
      System.out.println("\tIn The Style Of: " + style);
 }

如果我们开始通过调用super.Print来开始Print方法,而不是在前面的截图中显示的原始显式打印行,我们将从我们的Print方法中获得相同的行为。现在,当PoemPrint方法运行时,它将首先调用超类的,也就是Literature.java类的Print方法。最后,它将打印出Poem类的风格,这种风格并不适用于所有文学作品。

虽然我们的Poem构造函数和Literature构造函数具有不同的名称,甚至不同的输入样式,但PoemLiterature之间共享的两个Print方法是完全相同的。我们稍后会详细讨论这一点,但现在你应该知道我们在这里使用了一种叫做覆盖的技术。

覆盖

当我们声明一个子类具有与其超类方法相同的方法时,我们已经覆盖了超类方法。当我们这样做时,最好使用 Java 的Override指示符:

@Override public void Print()

这是对未来编码人员和我们编译套件的一些更深奥的元素的一个指示,即给定在前面的截图中的方法下隐藏了一个方法。当我们实际运行我们的代码时,Java 会优先考虑方法的最低或子类版本。

所以让我们看看我们是否成功声明了我们的PoemLiterature关系。让我们回到我们程序的Inheritence.java类的main方法,看看这个程序的诗歌部分是否像以前一样执行:

当我们运行这个程序时,我们得到了与之前完全相同的输出,这表明我们已经以合理的方式设置了我们的Poem类从Literature继承。

现在我们可以跳到我们的Book类。我们将按照以下步骤将其设置为BookLiterature类之间的 is-a 关系:

  1. 首先,我们将声明Book扩展Literature类;然后,我们将在我们的Book类中删除对标题和作者的引用,因为现在Literature类,即超类,将负责这一点:
        package inheritance;
        public class Book extends Literature{
        private String publisher;
        private String genre;
  1. Poem类一样,我们需要显式调用Literature类的构造函数,并将titleauthor传递给它:
        public Book(String title, String author, String publisher, String
        genre)
        {
             super(title, author);
             this.publisher = publisher;
             this.genre = genre;
         }
  1. 然后,我们可以利用我们的超类的Print方法来简化我们的Book类的打印:
        @Override public void Print()
        {
             super.Print();
             System.out.println("\tPublished By: " + publisher);
             System.out.println("\tIs A: " + genre);
  1. 再次,让我们跳回到我们的main方法并运行它,以确保我们已经成功完成了这个任务!

我们成功了:“指环王”的输出,就像我们以前看到的那样。在风格上,这个改变真的很棒。通过添加Literature类,然后对其进行子类化以创建BookPoem类,我们使得我们的BookPoem类更加简洁,更容易让程序员理解发生了什么。

然而,这种改变不仅仅是风格上的。通过声明BookPoem类继承自Literature类的 is-a 关系,我们给自己带来了实际上以前没有的功能。让我们看看这个功能。如果我们回到我们的main方法,假设我们不是处理单个BookPoem类,而是处理一个需要存储在某种数据结构中的庞大网络。使用我们最初的实现,这将是一个真正的挑战。

数据结构

没有一个易于访问的数据结构可以愉快地存储书籍和诗歌。我们可能需要使用两种数据结构或打破强类型,这正是 Java 的全部意义所在:

Book[] books = new Book[5];

然而,通过我们的新实现,BookPoem都继承自Literature,我们可以将它们存储在相同的数据结构中。这是因为继承是一种 is-a 关系,这意味着一旦我们从某物继承了,我们可以宣称书是文学,诗歌也是文学。如果这是真的,那么Literature对象的数组应该能够在其中存储BookPoem。让我们按照以下步骤来说明这一点:

  1. 创建一个Literature对象的数组:
 Literature[] lits = new Literature[5];
 lits[0] = a;
 lits[1] = b;

当我们构建这个项目时没有编译错误,这是一个非常好的迹象,表明我们正在做一些合法的事情。

  1. 为了进行演示,让我们在这里扩展我们的数组,以包含书籍和诗歌的数量:
 Literature[] lits = new Literature[5];
 lits[0] = a;
 lits[1] = b;
 lits[2] = a;
 lits[3] = b;
 lits[4] = a;

我们将修改我们的main方法,直接从数组中打印出来。现在,当我们像使用它们的超类对象一样使用我们的子类时,我们必须意识到我们现在是将它们作为该超类的对象引用。例如,当我们遍历并从我们的Literature数组中获取一个元素时,无论该元素是Book类,我们仍然无法访问诸如其genre字段之类的东西,即使这个字段是public

 Literature[] lits = new Literature[5];
 lits[0] = a;
 lits[1] = b;
 lits[2] = a;
 lits[3] = b;
 lits[4] = a;
 for(int i=0; i< lits.length; i++)
 {
      lits[i].Print(); 
 }

这是因为我们现在使用的Literature类作为一个对象(如前面的截图所示)没有genre成员变量。但我们可以调用超类中被子类重写的方法。

  1. 我们可以在我们的for循环中调用Literature类的Print方法。Java 将优先考虑我们子类的Print方法:
for(int i=0; i< lits.length; i++)
{
     lits[i].Print(); 
 }

这意味着,当我们运行这个程序时,我们仍然会得到我们归因于BookPoem的特殊格式化输出,而不是我们存储在Literature类中的简化版本:

public void Print()
{
     System.out.println(title);
     System.out.println("\tWritten By: " + author);
 }

抽象方法

我们有时会看到一些方法只存在于被子类重载。这些方法什么也不做,我们可以在超类(Literature.java)中使用abstract关键字标记它们,即public abstract void Print()。当然,如果一个类有声明为abstract的方法,这可能是一个好迹象,即这样的类的实例应该永远不会被显式创建。如果我们的Literature类的Print方法是抽象的,我们就不应该声明只是Literature的对象。我们应该只使用Literature的子类的对象。如果我们要走这条路,我们也应该将Literature声明为一个abstract类:

package inheritance;
public abstract class Literature {

当然,如果我们这样做,我们就必须摆脱对Literature类的超级方法的引用,所以现在让我们撤销这些更改。

让我们看一下我们在最初构建这个程序时犯的一个小错误。在创建我们的 Literature 类时,我们声明了titleauthorpublic成员变量。你可能知道,通常情况下,如果没有充分的理由,我们不会声明成员变量为 public。一旦宣布了,文学作品改变其作者并没有太多意义,所以authortitle应该是private成员变量,它们在Literature类的构造函数中设置,其值不应该改变。不幸的是,如果我们对我们的 Literature 类进行这种更改,我们将限制我们的 Poem 和 Book 类的功能。

比如说,我们想要修改Poem类的Print函数,这样它就不必显式调用Literature类的Print函数了:

@Override public void Print()
{
     System.out.println(title);
     System.out.println("\tWritten By: " + author);
     System.out.println("\tIn The Style Of: " + style);
 }

也许我们想要通过声明在这里创建一个Poem类来开始它:

System.out.println("POEM: " + title);

不幸的是,因为我们已经将titleauthor私有化到Literature类中,即使Poem类是Literature的子类,也无法在其显式代码中访问这些成员变量。这有点烦人,似乎在privatepublic之间有一种保护设置,它对于类的子类来说是私有的。实际上,有一种保护设置可用。

受保护的方法

protected方法是受保护的保护设置。如果我们声明成员变量为protected,那么它意味着它们是私有的,除了类和它的子类之外,其他人都无法访问:

package inheritance;
public class Literature {
    protected String title;
    protected String author;

只是为了让自己放心,我们在这里所做的一切都是合法的。让我们再次运行我们的程序,确保输出看起来不错,事实也是如此。之后,我们应该对继承有相当好的理解。我们可以开发很多系统,这些系统真正模拟它们的现实世界对应物,并且我们可以使用继承和小类编写非常优雅和功能性的代码,这些小类本身并不做太多复杂的事情。

抽象

在这一部分,我们将快速了解与 Java 中继承相关的一个重要概念。为了理解我们要讨论的内容,最好是从系统中的现有项目开始。让我们来看看代码文件中的代码。

到目前为止,我们已经做了以下工作:

  • 我们程序的main方法创建了一个对象列表。这些对象要么是Book类型,要么是Poem类型,但我们将它们放在Literature对象的列表中,这让我们相信BookPoem类必须继承或扩展Literature类。

  • 一旦我们建立了这个数组,我们只需使用for循环迭代它,并在每个对象上调用这个for循环的Print方法。

  • 在这一点上,我们处理的是Literature对象,而不是它们在最低级别的书籍或诗歌。这让我们相信Literature类本身必须实现一个Print方法;如果我们跳进类,我们会看到这确实是真的。

然而,如果我们运行我们的程序,我们很快就会看到书籍和诗歌以稍有不同的方式执行它们的Print方法,为每个类显示不同的信息。当我们查看BookPoem类时,这一点得到了解释,它们确实扩展了Literature类,但每个类都覆盖了Literature类的Print方法,以提供自己的功能。这都很好,也是一个相当优雅的解决方案,但有一个有趣的案例我们应该看一看并讨论一下。因为Literature本身是一个类,我们完全可以声明一个新的Literature对象,就像我们可以为BookPoem做的那样。Literature类的构造函数首先期望文学作品的title,然后是author。一旦我们创建了Literature类的新实例,我们可以将该实例放入我们的Literature类列表中,就像我们一直在做的BookPoem类的实例一样:

Literature l= new Literature("Java", "Zach");
Literature[] lits = new Literature[5];
lits[0] = a;
lits[1] = b;
lits[2] = l;
lits[3] = b;
lits[4] = a;
for(int i=0; i< lits.length; i++)
{
     lits[i].Print(); 
 }

当我们这样做并运行我们的程序时,我们将看到Literature类的Print方法被执行,我们创建的新Literature对象将显示在我们的书籍和诗歌列表旁边:

那么问题在哪里呢?嗯,这取决于我们试图设计的软件的真正性质,这可能有很多道理,也可能没有。假设我们正在作为图书馆系统的一部分进行这项工作,只提供某人所谓的 Java 是由某个叫 Zach 的人写的这样的信息,而不告诉他们它是一本书还是一首诗或者我们决定与特定类型的文学相关联的任何其他信息。这可能根本没有用,而且绝对不应该这样做。

如果是这样的话,Java 为我们提供了一个可以用于继承目的的类创建系统,但我们将永远无法合法地单独实例化它们,就像我们以前做的那样。如果我们想标记一个类为那种类型,我们将称其为abstract类,并且在类的声明中,我们只需使用abstract关键字。

public abstract class Literature {

一旦我们将一个类标记为abstract,实例化这个类就不再是一个合法的操作。乍一看,这是一件非常简单的事情,主要是一种“保护我们的代码免受自己和其他程序员的侵害”的交易,但这并不完全正确;它是正确的,但这并不是将一个类声明为abstract的唯一目的。

一旦我们告诉 Java,我们永远不能创建一个单独的Literature实例,只能使用Literature作为它们的超类的类,当设置Literature类时,我们就不再受到限制。因为我们声明Literature是一个抽象类,我们和 Java 都知道Literature永远不会单独实例化,只有当它是一个正在实例化的类的超类时才会实例化。在这种情况下,我们可以不需要大部分 Java 类必须具有的这个类的部分。例如,我们不需要为Literature实际声明构造函数。如果Literature是一个标准的 Java 类,Java 不会接受这一点,因为如果我们尝试实例化Literature,它将不知道该怎么做。将没有构造函数可供调用。但是因为Literature是抽象的,我们可以确信Literature的子类将有自己的构造函数。当然,如果我们做出这个改变,我们将不得不摆脱子类中对Literature构造函数的引用,也就是删除子类中的super方法。因此,这个改变肯定是有所取舍的。这需要更多的代码在我们的子类中,以减少我们的Literature超类中的代码。在这种特定情况下,这种权衡可能不值得,因为我们在BookPoem构造函数之间重复了代码,但如果可以假定Literature子类的构造函数做的事情非常不同,不声明一个共同的基础构造函数就是有意义的。

因此,简而言之,当我们设计我们的程序或更大的解决方案时,我们应该将那些在架构目的上非常合理但永远不应该单独创建的类声明为abstract。有时,当某些常见的类功能,比如拥有构造函数,对于这个类来说根本就没有意义时,我们真的会知道我们遇到了这样的类。

摘要

在本章中,我们了解了面向对象编程的一些复杂性,通过精确地使用继承的概念,创建了一个称为超类和子类的东西,并在它们之间建立了“是一个”关系。我们还讨论了一些关键方面的用法,比如覆盖子类和超类、数据结构和protected方法。我们还详细了解了abstract方法的工作原理。

在下一章中,您将了解有用的 Java 类。

第八章:有用的 Java 类

一旦我们对 Java 的基础知识,包括 Java 语法和 Java 构建的基本面向对象概念,有了一定的信心,我们就可以看一下 Java 的 API 和类库,这些对我们来说是立即和轻松地可访问的,用于编写 Java 程序。我们要这样做是因为我们将使用这些类库来加快我们的编程速度,并利用那些编写了非常棒东西的程序员的工作。

此外,查看 Java 类库,或者任何编程语言的类库,也是了解编程语言设计用途以及该语言中最佳编码应该是什么样子的好方法。

因此,在本章中,我们将看一下Calendar类及其工作原理。我们将深入研究String类及其一些有趣的方法。接下来,我们将介绍如何检测异常,即程序中的异常情况,以及如何处理它们。我们将看一下Object类,它是 Java 中所有类的超类。最后,我们将简要介绍 Java 的原始类。

本章将涵盖以下主题:

  • Calendar 类

  • String类以及使用String对象和文字之间的区别

  • 异常及如何处理它们

  • Object

  • Java 的原始类

Calendar 类

在本节中,我们将看一下 Java 的Calendar类。在编写 Java 代码时,我们通常使用Calendar类来指代特定的时间点。

Calendar类实际上是 Java API 的一个相对较新的添加。以前,我们使用一个叫做Date的类来执行类似的功能。如果你最终要处理旧的 Java 代码,或者编写涉及 SQL 或 MySQL 数据库的 Java 代码,你可能会偶尔使用 Java 的Date类。如果发生这种情况,不要惊慌;查阅 Java 文档,你会发现有一些非常棒的函数可以在CalendarDate对象之间进行切换。

为了看到 Java 的Calendar类的强大之处,让我们跳入一个 Java 程序并实例化它。让我们创建一个新程序;首先,从java.util包中导入所有类,因为Calendar类就在那里。

接下来,我们声明一个新的Calendar对象;我将其称为now,因为我们的第一个目标是将这个Calendar对象的值设置为当前时刻。让我们将now的值设置为Calendar对象的默认值,并看看它给我们带来了什么。为了做到这一点,我想我们需要使用new关键字。虽然我们实际上还没有在文档中查找过,但这似乎是一个合理的起始或默认日期,用于Calendar实例。

最后,让我们设置我们的程序,以便打印出我们的now对象中包含的信息:

package datesandtimes; 

import java.util.*; 

public class DatesAndTimes { 
    public static void main(String[] args) { 
        Calendar now = new Calendar(); 
        System.out.println(now); 
    } 

} 

也许令人惊讶的是,当我们尝试编译这个基本程序时,它实际上失败了:

我们的错误出现在Calendar上,我们已经实例化了Calendar类,根据控制台显示的错误。错误是Calendar是抽象的,不能被实例化。

如果你还记得,抽象类是那些纯粹设计为被子类化的类,我们永远不能单独声明抽象类的实例。那么如果我们永远不能实例化 Java 的Calendar类,那么它有什么好处呢?当然,这不是一个公平的问题,因为我们绝对可以创建Calendar对象;它们只是特定类型的Calendar对象。我们几乎总是会使用GregorianCalendar

Calendar 的子类

让我们退一步,假设也许是正确的,我们不知道Calendar有哪些选项可用。这是使用IDE(集成开发环境),比如 NetBeans,真的很棒的时候之一。

通常,在这个时间点上,我们需要查看 Java 文档,以确定Calendar的子类有哪些可以实例化。但是因为我们的 IDE 知道我们已经导入的包的一些元数据,我们可以询问我们的 IDE 它认为可能是我们代码的一个可能解决方案。如果你在 NetBeans 中工作,你可以通过从工具|选项|代码完成中检查一些代码完成选项来经常获得这些类型的建议。

然而,为了防止代码完成一直弹出,我将在这个场合使用 NetBeans 的快捷方式。默认情况下,这个快捷键组合是Ctrl + space,这将在我们光标当前位置弹出一个代码完成弹出窗口,如下面的屏幕截图所示:

NetBeans 中的代码完成选项非常出色。NetBeans 给了我们三个可能的建议:抽象的Calendar类,BuddhistCalendarGregorianCalendar。我们已经知道我们不想使用Calendar类,因为我们实际上不能实例化一个抽象类。BuddhistCalendarGregorianCalendar看起来确实是Calendar的子类。

如果我们选择GregorianCalendar,我们会看到它是Calendar的一个子类。所以让我们试着创建一个全新的GregorianCalendar实例,使用默认的设置和值:

package datesandtimes; 

import java.util.*; 

public class DatesAndTimes { 
    public static void main(String[] args) { 
        Calendar now = new GregorianCalendar(); 
        System.out.println(now); 
    } 

} 

如果我们运行这个 Java 程序,我们确实会得到一些输出:

这个输出意味着两件事:

  • 我们的语法是正确的,因为我们成功编译了

  • 我们可以看到 Java 在一个全新的Calendar对象中放入了什么值

Java 的一个很棒的地方是它要求新对象实现toString()方法,这个方法被println()使用。这意味着大多数 Java 标准库对象在我们要求它们打印自己时,能够以某种人类可读的格式打印出来。

我们这里打印出的新的Calendar类并不容易阅读,但我们可以浏览一下,看到许多字段已经被赋值,我们还可以看到Calendar类实际上有哪些字段(比如areFieldsSetareAllFieldsSet等)。

获取当前的日,月和年

让我们看看如何从Calendar类中获取一个信息。让我们看看它是否实际上被设置为今天的值。让我们将日,月和年分别打印在三行println上,以保持简单。要访问当前的日,月和年,我们需要从nowCalendar对象中获取这些字段。如果我们的Calendar对象表示特定的时间点,它应该有日,月和年的字段,对吧?如果我们打开自动完成选项,我们可以看到我们的Calendar对象公开给我们的所有字段和方法,如下面的屏幕截图所示:

我们不会找到一个容易访问的日,月和年字段,这可能开始让我们对Calendar感到失望;然而,我们只是没有深入到足够的层次。

Calendar类公开了get()方法,允许我们获取描述特定Calendar实例或时间点的字段。这是一个以整数作为参数的函数。对于我们中的一些人来说,这一开始可能看起来有点混乱。为什么我们要提供一个整数给get(),告诉它我们正在寻找哪个Calendar字段?

这个整数实际上是一个枚举器,我们暂时将其视为Calendar类本身公开的静态字符串。如果我们在get()的参数中输入Calendar类名,就像我们想要获取一个静态成员变量,然后返回自动完成,我们会看到我们可以在这个实例中使用的选项列表,如下面的屏幕截图所示:

其中一些选项并不太合理。我们必须记住,自动完成只是告诉我们Calendar公开的内容;它并不给我们解决方案,因为它不知道我们想要做什么。例如,我们不希望使用我们的Calendar实例now来获取其May的值;这没有任何意义。但是,我们可以使用我们的Calendar实例来获取当前月份(MONTH)。同样,我们真正想要的是当月的日期(DAY_OF_MONTH)和当前年份(YEAR)。让我们运行以下程序:

package datesandtimes; 

import java.util.*; 

public class DatesAndTimes { 
    public static void main(String[] args) { 
        Calendar now = new GregorianCalendar(); 
        System.out.println(now.get(Calendar.MONTH)); 
        System.out.println(now.get(Calendar.DAY_OF_MONTH)); 
        System.out.println(now.get(Calendar.YEAR)); 
    } 

} 

如果我们运行上述程序,我们得到输出9122017

我写这本书是在 2017 年 10 月 12 日,所以这实际上有点令人困惑,因为十月是一年中的第十个月。

幸运的是,对此有一个合理的解释。与一年中的日期和年份不同,它们可以存储为整数变量,大多数编程语言中的Calendar和类似Calendar的类的大多数实现(不仅仅是 Java)选择将月份存储为数组。这是因为除了数值之外,每个月还有一个相应的字符串:它的名称。

由于数组是从零开始的,如果你忘记了这一点,我们的月份看起来比它应该的要低一个月。我们的println()函数可能应该如下所示:

System.out.println(now.get(Calendar.MONTH) + 1); 

我得到了以下输出。你得相信我;这是今天的日期:

因此,Calendar有很多与之关联的方法。除了使用get()函数将Calendar设置为当前时间点并从中读取外,我们还可以使用set()函数将Calendar设置为时间点。我们可以使用add()函数添加或减去负值来指定时间点。我们可以使用before()after()函数检查时间点是在其他时间点之前还是之后。

日历的工作原理

然而,如果像我一样,你想知道这个Calendar对象是如何运作的。它是将月份、日期和时间秒存储在单独的字段中,还是有一个包含所有这些信息的大数字?

如果我们花一些时间查看Calendar类实现中可用的方法,我们会发现这两个方法:setTimeInMillis()及其姐妹方法getTimeInMillis()如下截图所示:

这些方法被特别设置是一个很好的机会,让我们看看Calendar类的真正思维方式。

让我们通过调用getTimeInMillis()函数并打印其输出来开始我们的探索:

System.out.println(now.getTimeInMillis()); 

我们得到了一个非常大的整数,这很可能是自某个特定时间以来的毫秒数:

如果我们进行数学计算,我们会发现这个时间点实际上不是公元元年;相反,它的时间要比那更接近。Calendar类称这个时间点为纪元,这是我们开始计算的时间点,当我们在 Java 中存储时间时,我们计算了多少毫秒自纪元以来。

我们可以使用计算器通过一个相当费力的过程来准确计算这个时间点,或者我们可以在我们的本地 Java 环境中以更少的痛苦来做。让我们简单地将now的值更改为0时的时间点,最初设置为默认或当前时间点。我们将使用setTimeInMillis()并提供0作为参数:

package datesandtimes; 

import java.util.*; 

public class DatesAndTimes { 
    public static void main(String[] args) { 
        Calendar now = new GregorianCalendar(); 

 now.setTimeInMillis(0); 

        System.out.println(now.getTimeInMillis()); 
        System.out.println(now.get(Calendar.MONTH) + 1); 
        System.out.println(now.get(Calendar.DAY_OF_MONTH)); 
        System.out.println(now.get(Calendar.YEAR)); 
    } 

} 

当我们再次运行程序时,我们得到相同的输出字段:

我们输出的第一个数字是我们确认毫秒已设置为0。现在我们的Calendar时间是 1970 年 1 月 1 日。因此,一旦我们开始向我们的对象添加天数,我们将从 1970 年 1 月 2 日开始计算。这个时间点被 Java Calendar称为时代。

为什么这对我们来说是一个非常有趣的事情?这意味着我们可以将我们的Calendar类转换为这些毫秒值,然后将它们作为整数值相加、相减,我想还可以将它们作为整数值相乘和相除。这使我们能够在数学的本机格式上对它们进行各种操作。

最后,我想向您展示另一件事,因为这是一个语法上的细节,您可能不熟悉,也可能不会在第一时间认出。如果您回忆一下本节开头,我们说Calendar是一个抽象类;我们只能实例化特定类型的Calendar类。然而,通常情况下,我们不会指定我们要找的确切类型的日历;我们会要求Calendar类来决定这一点。

正如我们在枚举中看到的,除了具有对象级方法之外,Calendar类还提供了一些静态方法,我们可以通过引用Calendar类型名称来使用。其中一个方法是Calendar.getInstance(),它将为我们创建 Java 可以找到的最佳匹配Calendar类:

Calendar now = Calendar.getInstance(); 

在这种情况下,将是我们已经创建的相同的GregorianCalendar类。

字符串功能

在 Java 中处理字符串可能会有点令人困惑,因为它们确实是一个特殊情况。字符串与之相关联的是字符串字面值的概念,即双引号之间的字符序列。我们可以将它直接放入我们的 Java 程序中,Java 会理解它,就像它理解整数或单个字符一样。

与整数、字符和浮点数不同,Java 没有与这个字符串字面值相关联的原始关键字。如果我们想要的话,我们可能会得到的最接近的是字符数组;然而,通常情况下,Java 喜欢我们将字符串字面值与String类相关联。要更好地理解String类,请查看以下程序:

package strings; 

public class Strings { 

    public static void main(String[] args) { 
        String s1 = new String
         ("Strings are arrays of characters"); 
        String s2 = new String
         ("Strings are arrays of characters"); 

        System.out.println("string1: " + s1); 
        System.out.println("string2: " + s2); 
        System.out.println(s1 == s2); 

    } 
} 

Java 中的String类是特殊的。在某些方面,它就像任何其他类一样。它有方法,正如我们在代码行中看到的,我们定义了变量s1s2,它有一个构造函数。但是,我们可以对String类使用通常仅保留给字面值和基本类型的运算符。例如,在前面的程序中,我们将s1添加到字符串字面值string 1:中以获得有意义的结果。在处理 Java 对象时,这通常不是一个选项。

字符串字面值与字符串对象

Java 决定将String类的对象作为字符串字面值或真正的对象可以互换使用,这真的很强大。它给了我们比我们原本拥有的更多操作文本的选项,但它也有一些权衡。在处理String对象时,非常重要的是我们理解我们是在处理它的字符串值还是对象本身。这是因为我们可能会得到截然不同的行为。我们看到的前面的程序旨在说明其中一个实例。

这是一个非常简单的程序。让我们逐步进行并尝试预测其输出。我们首先声明并实例化两个String对象:s1s2。我们使用String构造函数(我们很快会谈到为什么这很重要),并简单地将相同的字符串字面值传递给这些新对象中的每一个。然后,我们要求我们的程序打印出这些值,以便我们可以进行视觉比较。但是,我们还要求我们的程序执行这个有趣的任务:使用双等号比较运算符 s1s2进行比较。在运行此程序之前,花一秒钟时间问自己,“你认为这个比较的结果会是什么?”。

当我运行这个程序时,我发现 Java 不相信s1s2的比较结果是true。我得到的结果是false

根据我们当时对s1s2的想法,输出要么是合理的,要么是令人困惑的。如果我们认为s1s2是由比较运算符比较的字符串文字,那么我们会感到非常困惑。我们会想知道为什么我们没有得到true的结果,因为分配给s1s2的字符串文字是相同的。

然而,如果我们把s1s2看作它们实际上是的对象,false的结果就更有意义了,因为我们询问 Java 的是,“这两个对象是相同的吗?”显然不是,因为它们都是创建两个不同新对象的结果。

这就是为什么我们喜欢在 Java 中尽可能使用equals()方法。几乎每个对象都实现了equals()方法,而且应该为每个对象编写equals()方法,以便逻辑上比较这些对象的值。

如果我们使用equals()方法比较我们的字符串,我们也比较它们包含的字符串文字值:

System.out.println(s1.equals(s2)); 

现在,如果我们执行我们的程序,我们得到的结果是true,而不是当我们试图看它们是否实际上是存储在内存的相同位置的相同对象时得到的false

字符串函数

这个String实现给了我们什么能力?嗯,我们知道我们可以添加或连接字符串,因为我们可以将它们作为文字进行操作。除了文字操作,我们还可以利用String类本身提供的所有功能。我们可以查看 Java 文档,了解可用的功能,或者我们可以始终使用 NetBeans 的代码完成功能进行检查。我应该在这里指出,我们甚至可以在字符串文字上使用String类的功能,如下面的屏幕截图所示:

replace()函数

你将在方法列表中看到的大多数方法都是相当不言自明的(toLowerCase()toUpperCase()等)。但为了确保我们都明白,让我们使用其中一个。让我们使用replace()replace()函数接受两个参数,这些参数可以是单个字符,也可以是字符串符合条件的字符序列。该方法简单地用第二个字符串或字符替换第一个字符串或字符的所有实例。让我们看下面的replace()示例:

package strings; 

public class Strings { 

    public static void main(String[] args) { 
        String s1 = new String
        ("Strings are arrays of  characters"); 
        String s2 = new String
        ("Strings are arrays of characters"); 

        System.out.println
        ("string1: " + s1.replace("characters", "char")); 
        System.out.println("string2: " + s2); 
        System.out.println(s1.equals(s2)); 
    } 
} 

当我们运行我们的程序时,我们看到我们修改了它的输出:

大多数这些方法只是修改返回的值。我们可以看到我们的程序仍然发现在代码的最后一行s1等于s2,这表明我们对replace()方法的调用没有修改s1的值。replace()方法只是返回修改后的值供我们的println()函数使用。

format()函数

也许,String类中最有趣的方法之一实际上是它的静态方法之一:String.format()。为了向您展示String.format()的强大功能,我想为我们的项目创建一个全新的功能类。因此,在屏幕左侧显示的文件系统中右键单击项目名称,在新建类中输入CustomPrinter.java

package strings; 

public class Strings { 

    public static void main(String[] args) { 
        CustomPrinter printer = new CustomPrinter("> > %s < <"); 

        String s1 = new String
        ("Strings are arrays of characters"); 
        String s2 = new String
        ("Strings are arrays of characters"); 

        printer.println
        ("string1: " + s1.replace("characters", "char")); 
        printer.println("string2: " + s2); 
    } 
} 

为了让你看到我们在设置CustomPrinter类时在做什么,让我们看一下我们将在main()方法中使用的预写代码。CustomPrinter类的想法是它将有一个以字符串作为输入的构造函数。这个输入字符串将格式化或包装我们使用CustomPrinter实例打印到控制台的任何字符串。我们将在CustomPrinter中实现System.out.println(),所以当我们想要利用它来格式化我们的文本时,我们可以直接调用printer.println()

在 Java 中格式化字符串时,我们使用一些特殊的语法。在我们的格式字符串中,我们可以用百分号(就像我们在代码中使用的%s)来预先标识字符fds。在String.format()函数方面,Java 将这些理解为我们的格式字符串中要插入其他信息的区域。

我们在代码中使用的格式字符串将用尖括号包装我们创建的任何字符串输出。这比简单地将字符串附加和前置更复杂,我们当然可以创建一个实现,允许我们向我们的格式化字符串添加多个部分。

接下来让我们编辑CustomPrinter.java文件。我们知道我们需要一个CustomPrinter构造函数,它接受一个格式字符串作为输入。然后,我们可能需要存储这个format字符串。所以让我们的构造函数接受提供的格式字符串,并将其存储以备后用在formatString变量中:

package strings; 

public class CustomPrinter { 
    private String formatString; 

    public CustomPrinter(String format) 
    { 
        formatString = format; 
    } 
} 

我们还声明了一个println()函数,据推测它将是一个void函数;它只会利用system.out.println()将某些东西打印到屏幕上。那个某些东西会有点复杂。我们需要拿到我们给定的格式字符串,并用println()函数提供的输入替换%s

我们使用了强大的String.format()静态函数,它接受两个参数:一个格式字符串和要格式化的数据。如果我们的格式字符串有多个要格式化的字符串,我们可以在String.format()中提供多个字段。这是一个可以接受任意数量输入的函数。但是,为了保持一切简单和顺利,我们只会假设我们的格式字符串只有一个输入实例。

一旦我们成功使用String.format()函数格式化了这个字符串,我们就会简单地将它打印到屏幕上,就像我们之前做的那样:

package strings; 

public class CustomPrinter { 
    private String formatString; 

    public CustomPrinter(String format) 
    { 
        formatString = format; 
    } 

    public void println(String input) 
    { 
        String formatted = String.format(formatString, input); 
        System.out.println(formatted); 
    } 
} 

当我们运行这个程序(我们需要运行我们有main()方法的类),我们会看到我们所有的输出都被正确地包裹在尖括号中:

当然,像这样扩展自定义打印机,以接受更多的各种输入,并且比我们创建的快速东西更加动态,是任何东西的基础,比如日志系统,或者终端系统,你将能够看到相同的信息片段包裹在消息周围。例如,我们可以使用这样的自定义打印机,在向用户发送任何消息后放置日期和时间。然而,细节需要被正确格式化,这样它们不仅仅是被添加在末尾,而是在它们之间有适当的间距等。

我希望你已经学到了一些关于字符串的知识。Java 处理它们的方式真的很强大,但和大多数强大的编程工具一样,你需要在基本水平上理解它们,才能确保它们不会回来咬你。

异常

有时,我们的代码可能会失败。这可能是我们犯了编程错误,也可能是最终用户以我们没有预料到的方式使用我们的系统。有时,甚至可能是硬件故障;很多错误实际上不能真正归因于任何一个单一的来源,但它们会发生。我们的程序处理错误情况的方式通常和它处理理想使用情况的方式一样重要,甚至更重要。

在这一部分,我们将看一下 Java 异常。使用 Java 异常,我们可以检测、捕获,并在某些情况下从我们的程序中发生的错误中恢复。当我们处理异常时,有一件非常重要的事情要记住。异常之所以被称为异常,是因为它们存在于处理特殊情况,即我们在最初编写代码时无法处理或无法预料到的情况。

异常修改了我们程序的控制流,但我们绝不应该将它们用于除了捕获和处理或传递异常之外的任何其他用途。如果我们试图使用它们来实现逻辑,我们将制作一个对我们来说很快就会变得非常令人困惑,并且对于任何其他试图理解它的程序员来说立即变得非常令人困惑的程序。

为了帮助我们探索 Java 异常,我已经为我们设置了一个基本程序来玩耍;这是一个可能失败的东西。它是一个永恒的循环,做了两件真正的事情。首先,它使用ScannernextFloat()函数从用户那里获取输入,然后将该输入打印回用户:

package exceptions; 

import java.util.*; 

public class Exceptions { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 

        while(true) { 
            System.out.print("Input a number: "); 
            float input = reader.nextFloat(); 
            System.out.println("You input the number: " + input); 
            System.out.println("\r\n"); 
        } 
    } 
} 

如果我们将浮点值准确地分配为该程序的输入,那么该程序理论上将永远运行,如下面的屏幕截图所示:

然而,如果我们犯了一个错误,并给这个程序一个字符串作为输入,nextFloat()函数将不知道该怎么处理它,就会发生异常:

当这种情况发生时,我们在控制台中会看到红色的文本。这些红色文本实际上是发送到System.err流中的。

分析控制台异常消息

让我们浏览输出文本并理解它的含义。它有两个重要的部分。输出文本的第一部分,即没有缩进的部分,是这个异常的标识符。它让我们知道异常已经被抛出并且发生在哪里。然后它告诉我们发生了什么类型的异常。您会注意到这个异常在java.util路径中被发现(输出的这部分看起来非常类似于我们是否将某些东西导入到我们的代码中或直接将其路径到外部库)。这是因为这个异常实际上是一个 Java 对象,我们的输出文本让我们确切地知道它是什么类型的对象。

这个异常测试的第二部分(缩进的部分)是我们称之为堆栈跟踪。基本上它是我们的程序中 Java 跳过的部分。堆栈跟踪的最底部是异常最初抛出的位置;在这种情况下,它是Scanner.java,位于第909行。

那不是我们的代码;那是为Scanner.java编写的代码,可能是nextFloat()方法所在的地方或nextFloat()方法调用的代码。

堆栈跟踪是代码的层次,所以一旦发生InputMismatchException,Java 就开始跳过这些代码层次或括号区域,直到最终达到代码所在的顶层,这在我们的情况下是Exceptions.java。这是我们创建的文件,它在堆栈跟踪的顶部。我们的Exception.java代码文件的第 11 行是 Java 能够处理或抛出这个异常的最后位置。

一旦达到第 11 行并且异常仍在向上传播,就没有其他处理了,因为它已经达到了我们程序的顶部。因此,异常最终通过打印到我们的System.err流并且我们的程序以结果1终止,这是一个失败的情况。

这对于调试目的来说非常好;我们知道我们必须去哪里找出程序出了什么问题,即Exceptions.java的第 11 行。但是,如果我们正在创建一个我们希望出于某种合理目的发布的程序,我们通常不希望我们的程序在发生次要错误时崩溃,特别是像这样的输入错误,这是用户偶尔会犯的错误。因此,让我们探讨一下如何处理异常。

处理异常

当 Java 被告知抛出异常时,它会停止执行当前的代码块,并开始跳级,直到异常被处理。这就是我们从Scanner.java类的第 909 行深处跳转到Exceptions.java的第 11 行的方式,这是我们的代码中发生异常的地方。如果我们的代码被另一个代码块执行,因为我们没有处理这个异常,所以不会打印到System.err,我们只会将异常抛到另一个级别。因此,他们会在堆栈跟踪中看到Exception.java的第 11 行。

然而,有时不断抛出异常是没有意义的。有时,我们希望处理异常情况,因为我们知道该如何处理它,或者因为,就像我们现在处理的情况一样,有比提供堆栈跟踪和异常名称更好的方式来告知用户出了什么问题。

此外,如果我们在这里处理异常,那么我们没有理由不能像什么都没有发生一样恢复我们的while循环。这个while循环的一个失败案例并不一定是终止我们的程序的理由。如果我们要处理异常情况,我们将使用try...catch代码块。

try 和 catch 块

在我们认为可能会抛出异常并且我们想处理异常的任何代码块中,我们将把该行代码包装在try块中。在大多数情况下,这不会影响代码的执行方式,除非在try块内发生异常。如果在try块内抛出异常,代码不会将异常传播到下一个级别,而是立即执行以下catch块中的代码。

请注意,catch块在执行之前需要更多的信息;它们需要知道它们要捕获的确切内容。我们可以通过简单地捕获Exception类的任何内容来捕获所有异常,但这可能不是一个公平的做法。关于异常处理有很多不同的思路,但一般来说,人们会同意你应该只捕获和处理你在某种程度上预期可能发生的异常。

在我们看到的例子中,我们知道如果我们通过用户输入提供无效信息,就会抛出InputMismatchException。因为当这种异常发生时,我们将打印一条消息,明确告诉用户请输入一个浮点数。,我们当然不希望捕获任何不是InputMismatchException的异常。因此,我们使用以下代码来捕获InputMismatchException

package exceptions; 

import java.util.*; 

public class Exceptions { 
    public static void main(String[] args) { 
        Scanner reader = new Scanner(System.in); 

        while(true) { 
            try{ 
              System.out.print("Input a number: "); 
              float input = reader.nextFloat(); 
              System.out.println("You input the number: " + input); 
              System.out.println("\r\n"); 

            } 
            catch(InputMismatchException e) 
            { 
                System.out.println
                ("Please enter a float number."); 
                System.out.println("\r\n"); 
            } 
        }  
    } 
} 

当我们运行这个程序时,首先我们必须快速测试它在一个良好的用例中是否正常工作,就像以前一样。然后,如果我们通过提供字符串输入导致InputMismatchException被抛出,我们应该看到我们的 catch 块执行,并且我们应该得到请输入一个浮点数。的响应:

现在,正如你所看到的,我们确实得到了那个响应,但不幸的是,我们一遍又一遍地得到了那个响应。我们无意中引入了一个更糟糕的错误。现在,我们的程序不是抛出异常并崩溃,而是进入了一个无限循环。

这是为什么会发生这种情况:我们的Scanner对象reader是一个流读取器,这意味着它有一个输入缓冲区供它读取。在正常的使用情况下,当我们的无限while循环执行时,我们的用户将浮点数添加到该输入缓冲区。我们提取这些内容,打印它们,然后返回循环的开始并等待另一个。然而,当该缓冲区中发现一个字符串时,我们调用nextFloat()函数的代码行会抛出一个异常,这没问题,因为我们用 catch 块捕获了它。

我们的 catch 块打印出一行文本,告诉用户他/她提供了无效的输入,然后我们回到 while 循环的开头。但是,我们reader对象缓冲区中的坏字符串仍然存在,因此当我们捕获异常时,我们需要清除该流。

幸运的是,这是我们可以处理的事情。一旦我们捕获并处理了异常,我们需要清除流读取器,只需获取其下一行并不使用其信息。这将从读取器中刷新Please enter a float number.行:

catch(InputMismatchException e) 
{ 
    System.out.println("Please enter a float number."); 
    System.out.println("\r\n"); 
} 

如果我们现在运行程序,我们会看到它处理并从失败的输入中恢复,我们给它一个字符串,这很酷:

让我们再讨论一些我们可以处理异常的事情。首先,在异常情况结束时清除我们的读取器是有很多意义的,但在任何尝试的情况结束时清除我们的读取器可能更有意义。毕竟,我们进入这个while循环的假设是读取器中没有新行。因此,为了实现这一点,我们有finally块。

最后的块

如果我们想要无论我们在try块中是否成功,都要执行一个案例,我们可以在catch块后面跟着finally块。finally块无论如何都会执行,无论是否捕获了异常。这是为了让您可以在系统中放置清理代码。清理代码的一个例子是清除我们的reader对象缓冲区,以便以后或其他程序员不会困惑。

异常不仅仅是一个简单的被抛出的对象;它们可能包含很多非常重要的信息。正如我们之前看到的,异常可能包含堆栈跟踪。让我们快速修改我们的程序,以便在它仍然提供用户友好的Please enter a float number.信息的同时,也打印出堆栈跟踪,以便程序员可以调试我们的程序。

通常,当我们编写用户将要使用的完成代码时,我们永远不希望出现他们能够看到像堆栈跟踪这样深的东西。对大多数计算机用户来说这很困惑,并且在某些情况下可能构成安全风险,但作为调试模式或开发人员的功能,这些详细的异常可能非常有用。

Exception类公开了一个名为printStackTrace()的方法,它需要一个流作为输入。到目前为止,我们一直在使用System.out作为所有输出,所以我们将为printStackTrace()方法提供System.out作为其流:

catch(InputMismatchException e) 
{ 
    System.out.println("Please enter a float number."); 
    e.printStackTrace(System.out); 
    System.out.println("\r\n"); 
} 

现在,当我们运行程序并给出一个错误的字符串时,我们会得到我们最初友好的异常文本代码。但是,我们仍然有堆栈跟踪,因此我们可以准确地看到错误的来源:

正如我之前提到的,异常处理是现代软件开发中一个非常深入的主题,但在本节结束时,您应该对基础知识有所了解。当您在代码中遇到异常或者在编写自己的代码时感到需要异常处理时,您应该做好充分的准备。

对象类

在本节中,我们将学习关于 Java 如何选择实现面向对象编程的一些非常重要的内容。我们将探索Object类本身。为了开始,我写了一个非常基本的程序:

package theobjectclass; 

public class TheObjectClass { 

    public static void main(String[] args) { 
        MyClass object1 = new MyClass("abcdefg"); 
        MyClass object2 = new MyClass("abcdefg"); 

        object1.MyMethod(); 
        object2.MyMethod(); 

        System.out.println("The objects are the same: " + 
        (object1 == object2)); 
        System.out.println("The objects are the same: " + 
        object1.equals(object2)); 
    } 

} 

该程序利用了一个名为MyClass的自定义类,并创建了这个类的两个实例:object1object2。然后,我们在这些对象上调用了一个名为MyMethod的 void 方法,该方法简单地打印出我们给它们的值。然后,程序比较了这些对象。

我们首先使用比较运算符(==)进行比较,检查这两个对象是否实际上是同一个对象。我们知道这不会是真的,因为我们可以看到这些对象是完全独立实例化的。它们共享一个类,但它们是MyClass类的两个不同实例。然后,我们使用equals()方法比较这些对象,在本节中我们将经常讨论这个方法。

当我们运行这个程序时,我们看到当使用比较运算符进行比较时,对象被发现不相同,这是我们所期望的。但是,我们还看到当它们使用equals()方法进行比较时,尽管这两个对象是在相同的参数下创建的,并且从它们的创建到现在做了完全相同的事情,但这两个对象被发现不相等。以下是上述代码的输出:

那么,当equals()方法发现对象不相等时,这意味着什么?我们应该问自己的第一个问题是,equals()方法来自哪里或者它是在哪里实现的?

如果我们按照MyClass类的定义,实际上找不到equals()方法,这是非常奇怪的,因为MyClass并没有声明从任何超类继承,但equals()直接在MyClass实例上调用。实际上,MyClass,就像所有的 Java 类一样,都继承自一个超类。在每个类继承树的顶部,都有Object类,即使它在我们的代码中没有明确声明。

如果我们前往 Java 文档(docs.oracle.com/javase/7/docs/api/java/lang/Object.html)并查找Object类,我们会找到这样的定义:“Object类是类层次结构的根。每个类都有Object作为超类。所有对象,包括数组,都实现了这个类的方法。”然后,如果我们滚动页面,我们会得到一个简短但非常重要的方法列表:

因为所有的 Java 对象都继承自Object类,我们可以安全地假设我们正在处理的任何 Java 对象都实现了这里的每个方法。在这些方法中,就包括我们刚刚讨论并试图找出其来源的equals()方法。这让我们非常清楚,MyClass正在从它的Object超类中继承equals()方法。

在对象级别上,equals()方法的定义非常模糊。它说:“指示某个其他对象是否等于这个对象。”在某种程度上,这种模糊性让我们作为程序员来决定在逐个类的基础上真正意味着什么是相等的。

假设我们做出决定,合理的决定,即如果它们包含的值相同,那么object1object2应该被确定为相等。如果我们做出这个决定,那么我们当前程序的实现就不太正确,因为它目前告诉我们object1object2不相等。为了改变这一点,我们需要重写MyClass中的equals()方法。

重写 equals()方法

覆盖Object类方法并不比覆盖任何其他超类的方法更困难。我们只需声明一个相同的方法,当我们处理MyClass对象时,这个特定的方法将在适当的时候被使用。重要的是要注意,equals()方法不以MyClass对象作为输入;它以任何对象作为输入。因此,在我们继续比较这个对象的值与我们当前MyClass对象的值之前,我们需要保护自己,并确保作为输入的对象实际上是一个MyClass对象。

为了做到这一点,让我们检查一些坏的情况,我们希望我们的程序只需返回false,甚至不比较这些对象的内部值:

  1. 如果我们得到的对象实际上没有被实例化,是一个指针,或者是一个空指针,我们只需返回false,因为我们实例化的MyClass对象与什么都不等价。

  2. 更困难的问题是:我们得到的用于比较的对象是MyClass的一个实例吗?让我们检查相反的情况;让我们确认这个对象不是MyClass的一个实例。instanceof关键字让我们看到一个对象在其库存中有哪些类。如果我们的instanceof语句不评估为true,我们只需返回false,因为我们将比较一个MyClass对象和一个不是MyClass对象的对象。

一旦我们成功地通过了这些障碍,我们就可以安全地假设我们可以将给定的对象转换为MyClass对象。现在我们只需比较它们包含的值字段并返回适当的值。让我们将以下代码写入我们的MyClass.java文件,并返回到我们的main()方法来运行它:

package theobjectclass; 

public class MyClass { 
    public String value; 
    public MyClass(String value) 
    { 
         this.value = value; 
         System.out.println
         ("A MyClass object was created with value:" + value); 
     } 
     public void MyMethod() 
     { 
        System.out.println
        ("MyMethod was called on a MyClass object with value: " + 
        value); 
      }  

      @Override 
      public boolean equals(Object obj) 
      { 
         if(obj == null) 
           return false; 

         if(!(obj instanceof MyClass)) 
         return false; 

         return value.equals(((MyClass)obj).value); 

       } 
} 

当我们运行这个程序时,我们会看到object1object2被发现相互等价:

其他 Object 方法

Object类声明了许多方法。除了equals()之外,一些重要的方法是hashCode()toString()。在本节中,我们不会实现hashCode(),因为它需要我们做比较复杂的数学运算,但我强烈建议你查看hashCode()的工作原理,方法是查看文档并探索它。

目前,让我们只知道一个对象的hashCode()方法应该返回一个描述该特定对象的整数值。在所有情况下,如果两个对象通过equals()方法被发现相等,它们的hashCode()函数也应该返回相同的整数值。如果两个对象不相等,就equals()方法而言,它们的hashCode()函数应该返回不同的值。

此时,我们应该熟悉toString()方法。这也是Object类中的一个方法,这意味着我们可以在任何单个对象上调用toString()方法。但是,在我们的自定义对象中,直到我们覆盖toString(),它可能不会返回有意义的、可读的信息。

当你学习 Java 时,我强烈建议你实现equals()toString(),即使是在你学习时编写的小测试类上也是如此。这是一个很好的习惯,并且它让你以 Java 相同的方式思考面向对象编程。当我们创建最终的软件项目,其中有其他程序员可能会使用的公共类时,我们应该非常小心,确保所有我们的类以可理解的方式正确实现这些方法。这是因为 Java 程序员希望能够利用这些方法来操作和理解我们的类。

基本类

在本节中,我想快速看一下 Java 中可用的原始类。在 Java 中,我们经常说字符串很特殊,因为它们有一个由双引号标识的文字解释;然而,我们主要通过String类与它们交互,而不是通过我们实际上无法使用的string原始类型。

然而,在标准的 Java 原始类型中,我们通常通过其原始类型方法与其交互。对于每种原始类型,我们都有一个相应的原始类。这些是IntegerCharacterFloat类等。在大多数情况下,我们创建一个实例然后在该实例上调用方法的显式使用并不是很有用,除非我们重写它们以创建自己的类。让我们看一下以下程序:

package the.primitiveclasses; 

public class ThePrimitiveClasses { 

    public static void main(String[] args) { 
        String s = "string"; 

        Character c = 'c'; 
    } 

} 

Character类的实例c给我们的方法主要是转换方法,如下面的屏幕截图所示,这些方法将自动发生,或者我们可以简单地进行转换:

请注意,compareTo()有时也很有用。如果给定的其他字符等于并且小于0或大于0,则返回整数值0,具体取决于两个字符在整数转换比例中相对于彼此的位置。

然而,通常我们可能会发现自己使用这些原始类的静态方法来操作或从原始类型的实例中获取信息。例如,如果我想知道我们的字符C是否是小写,我当然可以将它转换为整数值,查看 ASCII 表,然后看看该整数值是否落在小写字符的范围内。但是,这是一项繁重的工作:

Character原始类为我提供了一个静态函数isLowercase(),如前面的屏幕截图所示,它将告诉我一个字符是否是小写。让我们运行以下程序:

package the.primitiveclasses; 

public class ThePrimitiveClasses { 

    public static void main(String[] args) { 
        String s = "string"; 

        Character c = 'c'; 
        System.out.println(Character.isLowerCase(c)); 
    } 

} 

以下是前面代码的输出:

这确实是原始函数的要点。我们可以以相同的方式与其他文字类型及其原始类型进行交互:如果愿意,可以使用类与字符串交互。

当我们不需要原始类的功能时,应继续使用原始类型(例如,使用char而不是Character)。语法高亮功能的存在以及这些原始类型在各种语言中的统一外观使它们更加友好,便于程序员使用。

摘要

在本章中,我们看了 Java 的Calendar类来处理日期和时间。我们详细了解了String类。我们还了解了异常是什么,以及如何处理它们使我们的程序更加健壮。然后,我们走过了Object类及其一些方法。最后,我们看了 Java 的原始类。

在下一章中,我们将学习如何使用 Java 处理文件。

第九章:文件输入和输出

文件 I/O 功能是一个非常强大的工具,可以使现代编程中最困难和令人沮丧的任务之一,即在代码的逻辑上分离的实体之间传输信息,比原本更容易。话虽如此,在本章中,您将学习如何使用FileWriterBufferedWriterFileReaderBufferedReader类来编写和读取数据文件。我们还将看一下close()方法和Scanner类的用法。然后您将学习异常处理。最后,我们将看到 I/O 的另一个方面:Serializable类。

具体来说,我们将在本章中涵盖以下主题:

  • 向文件写入数据

  • 从文件读取数据

  • Serializable 类

向文件写入数据

这将是一个令人兴奋的章节。首先,我们将看看如何使用 Java 写入文件。为此,我们将声明一个数学序列,前 50 个数字将是数学序列的前两个数字的和。当我们运行以下程序时,我们将看到这 50 个数字打印到我们的System.out流中,并且我们将能够在控制台窗口中查看它们:

package writingtofiles; 

public class WritingToFiles { 
    public static void main(String[] args) { 
        for(long number : FibonacciNumbers()) 
        { 
            System.out.println(number); 
        } 
    } 

    private static long[] FibonacciNumbers() 
    { 
        long[] fibNumbers = new long[50]; 
        fibNumbers[0] = 0; 
        fibNumbers[1] = 1; 
        for(int i = 2; i < 50; i++) 
        { 
            fibNumbers[i] = fibNumbers[i - 1] + fibNumbers[i - 2]; 
        } 
        return fibNumbers; 
    } 
} 

然而,当我们永久关闭控制台时,这些数字将丢失。为了帮助我们完成这项任务,我们将利用java.io库;在这里,io代表输入和输出

import java.io.*; 

我们将利用这个库中的一个类:FileWriter

FileWriter 类

FileWriter类及其用法可以解释如下:

  1. 让我们声明一个新的FileWriter类,并且出于稍后会变得明显的原因,让我们明确地将这个FileWriter类设置为 null:
        public class WritingToFiles { 
            public static void main(String[] args) { 
                FileWriter out = null; 
  1. 一旦我们这样做,我们就可以继续实例化它。为了写入文件,我们需要知道两件重要的事情:
  • 首先,当然,我们需要知道要写入文件的内容

  • 其次,我们的FileWriter类需要知道它应该写入哪个文件

  1. 当我们使用FileWriter类时,我们将它与特定文件关联起来,因此我们将文件名传递给它的构造函数,我们希望它写入该文件。我们的FileWriter类能够在没有文件的情况下创建文件,因此我们应该选择一个以.txt结尾的名称,这样我们的操作系统就会知道我们正在创建一个文本文件:
        public class WritingToFiles { 
            public static void main(String[] args) { 
                FileWriter out = null; 
                    out = new FileWriter("out.txt"); 

尽管我们使用有效的参数调用了FileWriter构造函数,NetBeans 仍会告诉我们,我们在这段代码中会得到一个编译器错误。它会告诉我们有一个未报告的异常,即可能在此处抛出IOException错误。Java 中的许多异常都标记为已处理异常。这些是函数明确声明可能抛出的异常。FileWriter是一个明确声明可能抛出IOException错误的函数。因此,就 Java 而言,我们的代码不明确处理这种可能的异常是错误的。

  1. 当然,为了处理这个问题,我们只需用try...catch块包装我们使用FileWriter类的代码部分,捕获IOException错误:

  2. 如果我们捕获到IOException错误,现在可能是打印有用消息到错误流的好时机:

        catch(IOException e) 
        { 
             System.err.println("File IO Failed."); 
        } 

然后,我们的程序将完成运行,并且将终止,因为它已经到达了main方法的末尾。有了这个异常捕获,FileWriter的实例化现在是有效和合法的,所以让我们把它用起来。

我们不再需要我们的程序将数字打印到控制台,所以让我们注释掉我们的println语句,如下面的代码块所示:

        for(long number : FibonacciNumbers()) 
        { 
            // System.out.println(number); 
        } 

我们将用我们的FileWriter类做同样的逻辑处理:

            try{ 
                out = new FileWriter("out.txt"); 
                for(long number : FibonacciNumbers()) 
                { 
                    // System.out.println(number); 
                } 

FileWriter类没有println语句,但它有write方法。每当我们的foreach循环执行时,我们希望使用out.write(number);语法将数字写入我们的文件。

  1. 不幸的是,write方法不知道如何以“长数字”作为输入;它可以接受一个字符串,也可以接受一个整数。因此,让我们使用静态的String类方法valueOf来获取我们的“长数字”的值,以便将数字打印到我们的文件中:
        for(long number : FibonacciNumbers()) 
        { 
            out.write(String.valueOf(number)); 
            // System.out.println(number); 
        } 

因此,我们现在应该拥有一个成功的程序的所有部分:

    • 首先,我们声明并实例化了我们的FileWriter类,并给它一个文件名
  • 然后,我们循环遍历我们的斐波那契数列,并告诉我们的FileWriter类将这些数字写入out.txt

然而,问题是out.txt在哪里?我们没有给FileWriter类一个完整的系统路径,只是一个文件名。我们知道FileWriter类有能力创建这个文件,如果它不存在,但在我们系统的目录中,FileWriter类会选择在哪里创建这个文件?

要回答这个问题,我们需要知道 NetBeans 将为我们编译的程序创建.jar文件的位置。为了找出这一点,我们可以打开控制台窗口并构建我们的程序。在这里,NetBeans 会告诉我们它正在创建所有文件的位置。例如,在我的情况下,有一个名为WritingToFiles的文件夹;如果我们导航到这个文件夹,我们会看到我们的项目文件。其中一个文件是dist,缩写为可分发,这就是我们的 JAR 文件将被编译到的地方:

JAR 文件是我们能够获得的最接近原始 Java 代码的可执行文件。因为 Java 代码必须由 Java 虚拟机解释,我们实际上无法创建 Java 可执行文件;然而,在大多数安装了 Java 的操作系统中,我们可以通过双击运行 JAR 文件,就像运行可执行文件一样。我们还可以告诉 Java 虚拟机使用 Java 命令行-jar命令启动和运行 JAR 文件,后面跟着我们想要执行的文件的名称,当然:

当我们提交这个命令时,Java 虚拟机解释并执行了我们的WritingToFiles.jar程序。看起来好像成功了,因为在目录中创建了一个新文件,如前面的截图所示。这是工作目录,直到我们移动它,这就是执行 JAR 文件的命令将执行的地方。所以这就是我们的FileWriter类选择创建out.txt的地方。

使用 close()方法释放资源

不幸的是,当我们打开out.txt时,我们看不到任何内容。这让我们相信我们的文件写入可能没有成功。那么出了什么问题呢?嗯,使用FileWriter的一个重要部分我们没有考虑到。当我们创建我们的FileWriter时,它会打开一个文件,每当我们打开一个文件时,我们应该确保最终关闭它。从代码的角度来看,这是相当容易做到的;我们只需在我们的FileWriter上调用close方法:

public class WritingToFiles { 
    public static void main(String[] args) { 
        FileWriter out = null; 
        try{ 
            out = new FileWriter("out.txt"); 
            for(long number : FibonacciNumbers()) 
            { 
                out.write(String.valueOf(number)); 
                // System.out.println(number); 
            } 

        } 
        catch(IOException e) 
        { 
            System.err.println("File IO Failed."); 
        } 

        finally{ 
            out.close(); 
        } 
    } 

有一个熟悉的错误消息出现,如下面的截图所示;out.close也可以报告一个IOException错误:

我们可以将out.close放在另一个try...catch块中,并处理这个IOException错误,但如果我们的文件无法关闭,那就意味着有非常严重的问题。在这种情况下,将这个异常传播到更健壮的代码而不是我们相当封闭的WritingToFiles程序可能更合适。如果我们不处理这个异常,这将是默认的行为,但我们确实需要让 Java 知道从我们当前的代码中向上传播这个异常是可能的。

当我们声明我们的main方法时,我们还可以让 Java 知道这个方法可能抛出哪些异常类型:

public static void main(String[] args) throws IOException 

在这里,我们告诉 Java,在某些情况下,我们的main方法可能无法完美执行,而是会抛出IOException错误。现在,任何调用WritingToFilesmain方法的人都需要自己处理这个异常。如果我们构建了 Java 程序,然后再次执行它,我们会看到out.txt已经被正确打印出来。不幸的是,我们忘记在输出中加入新的行,所以数字之间没有可辨认的间距。当我们写入时,我们需要在每个数字后面添加\r\n。这是一个新的换行转义字符语法,几乎可以在每个操作系统和环境中看到:

for(long number : FibonacciNumbers()) 
{ 
    out.write(String.valueOf(number) + "\r\n"); 
    // System.out.println(number); 
} 

再次构建、运行并查看out.txt,现在看起来非常有用:

所以这是我们最初的目标:将这个斐波那契数列打印到一个文件中。在我们完成之前,还有一些事情要快速看一下。让我们看看如果我们再次运行程序会发生什么,然后看看我们的输出文本文件。文本文件看起来和之前一样,这可能是预期的,也可能不是。似乎FileWriter是否清除这个文件并写入全新的文本是一种抉择,或者它是否会在文件中现有文本后面放置追加的文本。默认情况下,我们的FileWriter会在写入新内容之前清除文件,但我们可以通过FileWriter构造函数中的参数来切换这种行为。比如,我们将其追加行为设置为true

try { 
    out = new FileWriter("out.txt", true); 

现在构建项目,运行它,并查看out.txt;我们会看到比以前多两倍的信息。我们的文本现在被追加到末尾。

BufferedWriter 类

最后,在 Java 中有很多不同的写入器可供我们使用,FileWriter只是其中之一。我决定在这里向你展示它,因为它非常简单。它接受一些文本并将其打印到文件中。然而,很多时候,你会看到FileWriterBufferedWriter类包裹。现在BufferedWriter类的声明将看起来像以下代码块中给出的声明,其中BufferedWriter被创建并给定FileWriter作为其输入。

BufferedWriter类非常酷,因为它会智能地接受你给它的所有命令,并尝试以最有效的方式将内容写入文件:

package writingtofiles; 

import java.io.*; 

public class WritingToFiles { 
    public static void main(String[] args) throws IOException { 
        BufferedWriter out = null; 

        try { 
            out = new BufferedWriter(new FileWriter
             ("out.txt", true)); 

            for(long number : FibonacciNumbers()) 
            { 
                out.write(String.valueOf(number) + "\r\n"); 
                //System.out.println(number); 
            } 
        } 
        catch(IOException e) { 
            System.err.println("File IO Failed."); 
        } 
        finally{ 
            out.close(); 
        } 
    } 

我们刚刚编写的程序从我们的角度来看,做的事情与我们现有的程序一样。然而,在我们进行许多小写入的情况下,BufferedWriter可能会更快,因为在适当的情况下,它会智能地收集我们给它的写入命令,并以适当的块执行它们,以最大化效率:

out = new BufferedWriter(new FileWriter("out.txt", true)); 

因此,很多时候你会看到 Java 代码看起来像前面的代码块,而不是单独使用FileWriter

从文件中读取数据

作为程序员,我们经常需要从文件中读取输入。在本节中,我们将快速看一下如何从文件中获取文本输入。

我们已经告诉 Java,有时我们的main方法会简单地抛出IOException错误。以下代码块中的FileWriterFileReader对象可能会因为多种原因创建多个IOException错误,例如,如果它们无法连接到它们应该连接的文件。

package inputandoutput; 

import java.io.*; 

public class InputAndOutput { 
    public static void main(String[] args) throws IOException { 
        File outFile = new File("OutputFile.txt"); 
        File inFile = new File("InputFile.txt"); 

        FileWriter out = new FileWriter(outFile); 
        FileReader in = new FileReader(inFile); 

        //Code Here... 

        out.close(); 
        in.close(); 
    } 
} 

在为实际应用编写实际程序时,我们应该始终确保以合理的方式捕获和处理异常,如果真的有必要,就将它们向上抛出。但是我们现在要抛出所有的异常,因为我们这样做是为了学习,我们不想现在被包裹在try...catch块中的所有代码所拖累。

FileReader 和 BufferedReader 类

在这里,您将通过我们已经有的代码(请参阅前面的代码)学习FileReader类。首先,按照以下步骤进行:

  1. 我已经为我们声明了FileWriterFileReader对象。FileReaderFileWriter的姊妹类。它能够,信不信由你,从文件中读取文本输入,并且它的构造方式非常相似。它在构造时期望被给予一个文件,以便在其生命周期内与之关联。

  2. 与其简单地给FileReaderFileWriter路径,我选择创建File对象。Java 文件对象只是对现有文件的引用,我们告诉该文件在创建时将引用哪个文件,如下面的代码块所示:

        package inputandoutput; 

        import java.io.*; 

        public class InputAndOutput { 
            public static void main(String[] args)
             throws IOException { 
                File outFile = new File("OutputFile.txt"); 
                File inFile = new File("InputFile.txt"); 

                FileWriter out = new FileWriter(outFile); 
                FileReader in = new FileReader(inFile); 

                //Code Here... 
                out.write(in.read()); 
                out.close(); 
                in.close(); 
            } 
        } 

在这个程序中,我们将使用包含一些信息的InputFile.txt。此外,我们将使用OutputFile.txt,目前里面没有信息。我们的目标是将InputFile中的信息移动到OutputFile中。FileWriterFileReader都有一些在这里会有用的方法。

我们的FileWriter类有write方法,我们知道可以用它来将信息放入文件中。同样,FileReaderread方法,它将允许我们从文件中获取信息。如果我们简单地按顺序调用这些方法并运行我们的程序,我们会看到信息将从InputFile中取出并放入OutputFile中:

不幸的是,OutputFile中只出现了一个字符:InputFile文本的第一个字符。看起来我们的FileReader类的read方法只获取了最小可获取的文本信息。不过这对我们来说并不是问题,因为我们是程序员。

  1. 我们可以简单地使用in.read方法循环遍历文件,以获取在InputFile文件中对我们可用的所有信息:
        String input = ""; 
        String newInput; 
        out.write(in.read()); 
  1. 然而,我们可以通过用BufferedReader类包装FileReader来使生活变得更加轻松。类似于我们用BufferedWriter包装FileWriter的方式,用BufferedReader包装FileReader将允许我们在任何给定时间收集不同长度的输入:
        FileWriter out = new FileWriter(outFile); 
        BufferedReader in = new BufferedReader(new FileReader(inFile)); 

与包装我们的FileWriter类一样,包装我们的FileReader类几乎总是一个好主意。BufferedReader类还可以保护FileReader类,使其不受FileReader类一次性无法容纳的过大文件的影响。这种情况并不经常发生,但当发生时,可能会是一个相当令人困惑的错误。这是因为BufferedReader一次只查看文件的部分;它受到了那个实例的保护。

BufferedReader类还将让我们使用nextLine方法,这样我们就可以逐行从InputFile中收集信息,而不是逐个字符。不过,无论如何,我们的while循环看起来都会非常相似。这里唯一真正的挑战是我们需要知道何时停止在InputFile文件中寻找信息。为了弄清楚这一点,我们实际上会在while循环的条件部分放一些功能代码。

  1. 我们将为这个newInput字符串变量分配一个值,这个值将是in.readLine。我们之所以要在while循环的条件部分进行这个赋值,是因为我们可以检查newInput字符串被分配了什么值。这是因为如果newInput字符串根本没有被分配任何值,那就意味着我们已经到达了文件的末尾:
        while((newInput = in.readLine()) !=null) 
        { 

        } 

如果newInput有一个值,如果变量不是空的,那么我们会知道我们已经从文件中读取了合法的文本,实际上是一整行合法的文本,因为我们使用了readLine方法。

  1. 在这种情况下,我们应该添加一行新的文本,即 input += newInput; 到我们的输入字符串。当我们执行完我们的 while 循环时,当 newInput 字符串被赋予值 null,因为读者没有其他内容可读时,我们应该打印出我们一直在构建的字符串:
        while((newInput = in.readLine()) != null) 
        { 
            input += newInput; 
        } 
        out.write(input); 
  1. 现在,因为我们的 BufferedReader 类的 readLine 方法专门读取文本行,它不会在这些行的末尾附加结束行字符,所以我们必须自己做:
         while((newInput = in.readLine()) != null) 
        { 
             input += (newInput + "\r\n"); 
        } 

所以,我们已经执行了这个程序。让我们去我们的目录,看看复制到 OutputFile 的内容:

好了;InputFileOutputFile 现在具有相同的内容。这就是 Java 中基本文件读取的全部内容。

还有一些其他需要注意的事情。就像我们可以用 BufferedReader 包装 FileReader 一样,如果我们导入 java.util,我们也可以用 Scanner 包装 BufferedReader

        Scanner in = new Scanner(new BufferedReader
        (new FileReader(inFile))); 

这将允许我们使用 Scanner 类的方法来获取我们正在读取的文本中与某些模式匹配的部分。还要注意的是,FileReader 类及其包装类只适用于从 Java 文件中读取文本。如果我们想要读取二进制信息,我们将使用不同的类;当您学习如何在 Java 中序列化对象时,您将看到更多相关内容。

可序列化类

通常,当我们处理实际代码之外的信息时,我们处理的是从文件中获取的或写入文件的人类可读的文本,或者来自输入或输出流的文本。然而,有时,人类可读的文本并不方便,我们希望使用更适合计算机的信息。通过一种称为序列化的过程,我们可以将一些 Java 对象转换为二进制流,然后可以在程序之间传输。这对我们来说不是一种友好的方法,我们将在本节中看到。对我们来说,序列化的对象看起来像是一团乱码,但另一个了解该对象类的 Java 程序可以从序列化的信息中重新创建对象。

然而,并非所有的 Java 对象都可以被序列化。为了使对象可序列化,它需要被标记为可以被序列化的对象,并且它只能包含那些本身可以被序列化的成员。对于一些对象来说,那些依赖外部引用或者那些只是没有所有成员都标记为可序列化的对象,序列化就不合适。参考以下代码块:

package serialization; 

public class Car { 
    public String vin; 
    public String make; 
    public String model; 
    public String color; 
    public int year; 

    public Car(String vin, String make, String model, String 
     color, int year) 
    { 
        this.vin = vin; 
        this.make = make; 
        this.model = model; 
        this.color = color; 
        this.year = year; 
    } 

    @Override  
    public String toString() 
    { 
        return String.format
         ("%d %s %s %s, vin:%s", year, color, make, model, vin); 
    } 
} 

在给定程序中的类(在上面的代码块中)是序列化的一个主要候选对象。它的成员是一些字符串和整数,这些都是 Java 标记为可序列化的类。然而,为了将 Car 对象转换为二进制表示,我们需要让 Java 知道 Car 对象也是可序列化的。

我们可以通过以下步骤来实现这一点:

  1. 我们将需要 io 库来实现这一点,然后我们将让 Java 知道我们的 Car 对象实现了 Serializable
        import java.io.*; 
        public class Car implements Serializable{ 

这告诉 Java,Car 对象的所有元素都可以转换为二进制表示。除非我们已经查看了对象并经过深思熟虑并确定这是一个安全的假设,否则我们不应该告诉 Java 对象实现了 Serializable

所以,我们现在将 Car 标记为 Serializable 类,但这当然是本节的简单部分。我们的下一个目标是利用这个新功能来创建一个 Car 对象,将其序列化,打印到文件中,然后再读取它。

  1. 为此,我们将创建两个新的 Java 类:一个用于序列化我们的对象并将其打印到文件中,另一个类用于反序列化我们的对象并从文件中读取它。

  2. 在这两个类中,我们将创建 main 方法,以便我们可以将我们的类作为单独的 Java 程序运行。

序列化对象

让我们从Serialize类开始,如下所示:

  1. 我们要做的第一件事是为我们序列化的对象。所以让我们继续实例化一个新的Car对象。Car类需要四个字符串和一个整数作为它的变量。它需要一个车辆识别号码、制造商、型号、颜色和年份。因此,我们将分别给它所有这些:
        package serialization; 
        public class Serialize { 
            public static void main(String[] args) { 
                Car c =  new Car("FDAJFD54254", "Nisan", "Altima",
                "Green", 2000); 

一旦我们创建了我们的Car对象,现在是时候打开一个文件并将这个Car序列化输出。在 Java 中打开文件时,我们将使用一些不同的管理器,这取决于我们是否想要将格式化的文本输出写入这个文件,还是我们只打算写入原始二进制信息。

  1. 序列化对象是二进制信息,所以我们将使用FileOutputStream来写入这些信息。FileOutputStream类是使用文件名创建的:
        FileOutputStream outFile = new FileOutputStream("serialized.dat"); 

因为我们正在写入原始二进制信息,所以指定它为文本文件并不那么重要。我们可以指定它为我们想要的任何东西。无论如何,我们的操作系统都不会知道如何处理这个文件,如果它尝试打开它的话。

我们将想要将所有这些信息包围在一个try...catch块中,因为每当我们处理外部文件时,异常肯定会被抛出。如果我们捕获到异常,让我们只是简单地打印一个错误消息:

            try{ 
                FileOutputStream outFile =  
                new FileOutputStream("serialized.dat"); 
            } 
            catch(IOException e) 
            { 
                 System.err.println("ERROR"); 
             } 

请注意,我们需要在这里添加很多输入;让我们只是导入整个java.io库,也就是说,让我们导入java.io.*;包。

现在我认为我们可以继续了。我们已经创建了我们的FileOutputStream类,这个流非常好。但是,我们可以用另一个更专门用于序列化 Java 对象的字符串来包装它。

  1. 这是ObjectOutputStream类,我们可以通过简单地将它包装在现有的FileOutputStream对象周围来构造ObjectOutputStream对象。一旦我们创建了这个ObjectOutputStream对象并将文件与之关联,将我们的对象序列化并将其写入这个文件变得非常容易。我们只需要使用writeObject方法,并提供我们的Car类作为要写入的对象。

  2. 一旦我们将这个对象写入文件,我们应该负责关闭我们的输出字符串:

        try{ 
            FileOutputStream outFile = new
            FileOutputStream("serialized.dat"); 
            ObjectOutputStream out = new ObjectOutputStream(outFile); 
            out.writeObject(c); 
            out.close(); 
        } 
        catch(IOException e) 
        { 
             System.err.println("ERROR"); 
         } 

现在我认为我们可以运行我们接下来的程序了。让我们看看会发生什么:

        package serialization; 

        import java.io.*; 

        public class Serialize { 
            public static void main(String argv[]) { 
                Car c = new Car("FDAJFD54254", "Nisan", "Altima",
                "Green", 2000); 

                try { 
                    FileOutputStream outFile = new
                    FileOutputStream("serialized.dat"); 
                    ObjectOutputStream out = new
                    ObjectOutputStream(outFile); 
                    out.writeObject(c); 
                    out.close(); 
                } 
                catch(IOException e) 
                { 
                    System.err.println("ERROR"); 
                } 
            }
        } 

在这个 Java 项目中,我们有多个main方法。因此,就 NetBeans 而言,当我们运行程序时,我们应该确保右键单击要输入的main方法的类,并专门运行该文件。当我们运行这个程序时,我们实际上并没有得到任何有意义的输出,因为我们没有要求任何输出,至少没有抛出错误。但是,当我们前往这个项目所在的目录时,我们会看到一个新文件:serialize.dat。如果我们用记事本编辑这个文件,它看起来相当荒谬:

这肯定不是一种人类可读的格式,但有一些单词,或者单词的片段,我们是能够识别的。它看起来肯定是正确的对象被序列化了。

反序列化对象

让我们从我们的另一个类开始,也就是DeSerialize类,并尝试编写一个方法,从我们已经将其序列化信息写入的文件中提取Car对象。这样做的步骤如下:

  1. 再一次,我们需要一个Car对象,但这一次,我们不打算用构造函数值来初始化它;相反,我们将把它的值设置为我们从文件中读取回来的对象。我们在反序列化器中要使用的语法将看起来非常类似于我们在Serializemain方法中使用的语法。让我们只是复制Serialize类的代码,这样我们就可以在构建DeSerialize类的main方法时看到镜像相似之处。

在之前讨论的Serialize类中,我们在Serialize类的方法中犯了一个不负责任的错误。我们关闭了ObjectOutputStream,但没有关闭FileOutputStream。这并不是什么大问题,因为我们的程序立即打开了这些文件,执行了它的功能,并在终止 Java 时销毁了这些对象,文件知道没有其他东西指向它们。因此,我们的操作系统知道这些文件已关闭,现在可以自由地写入。但是,在一个持续很长时间甚至无限期的程序中,不关闭文件可能会产生一些非常奇怪的后果。

当我们像在这个程序中所做的那样嵌套FileInputOutput类时,通常会以我们访问它们的相反顺序关闭文件。在这个程序中,我们在调用out.close之前调用outFile.close是没有意义的,因为在这一瞬间,我们的ObjectOutputStream对象将引用一个它无法访问的文件,因为内部的FileOutputStream类已经关闭了。现在删除Car c = new Car("FDAJFD54254", " Nisan", "Altima", "Green", 2000);在当前的DeSerialize.java类中。

搞定了这些,我们已经复制了我们的代码,现在我们要对其进行一些修改。所以,我们现在不是将对象序列化到文件中,而是从文件中读取序列化的对象。

  1. 因此,我们将使用它的姐妹类FileInputStream,而不是FileOutputStream
        FileInputStream outFile = new FileInputStream("serialized.dat"); 
  1. 让我们再次导入java.io。我们希望引用与前面的代码中给出的相同的文件名;另外,让我们聪明地命名我们的变量。

  2. 以类似的方式,我们将FileInputStream包装为ObjectInputStream,而不是ObjectOutputStream,它仍然引用相同的文件:

        ObjectInputStream in = new ObjectInputStream(outFile); 

当然,这一次我们对将对象写入文件没有兴趣,这很好,因为我们的InputStream类没有权限或知识来写入这个文件;然而,它可以从文件中读取对象。

  1. ReadObject不需要任何参数;它只是简单地读取那个文件中的任何对象。当它读取到那个对象时,将其赋给我们的Car对象。当然,ReadObject只知道它将从文件中获取一个对象;它不知道那个对象的类型是什么。序列化的一个弱点是,我们确实被迫去相信并将这个对象转换为预期的类型:
         c = (Car)in.readObject(); 
  1. 一旦我们这样做了,就是时候以相反的顺序关闭我们的文件读取器了:
        try { 
            FileInputStream inFile = new FileInputStream("serialized.dat"); 
            ObjectInputStream in = new ObjectInputStream(inFile); 
            c = (Car)in.readObject(); 
            in.close(); 
            inFile.close(); 
        } 
  1. 现在有另一种被处理的异常类型,即ClassNotFoundException

如果我们的readObject方法失败,就会抛出这个异常。

所以,让我们捕获ClassNotFoundException,为了保持简单和流畅,我们将像处理之前的 I/O 异常一样,抛出或打印出错误消息:

            catch(ClassNotFoundException e) 

            { 
                System.err.println("ERROR"); 
            } 
  1. 现在我们需要一种方法来判断我们的程序是否工作。因此,在最后,让我们尝试使用自定义的toString函数打印出我们汽车的信息,也就是System.out.println(c.toString());语句。NetBeans 提示我们,变量c在这个时候可能尚未初始化,如下面的截图所示:

有些编程语言会让我们犯这个错误,我们的Car对象可能尚未初始化,因为这个try块可能已经失败了。为了让 NetBeans 知道我们意识到了这种情况,或者说,让 Java 知道我们意识到了这种情况,我们应该初始化我们的Car对象。我们可以简单地将其初始化为值null

            public class DeSerialize { 
                public static void main(String[] args) { 
                    Car c = null; 

                    try { 
                        FileInputStream inFile = new
                        FileInputStream("serialized.dat"); 
                        ObjectInputStream in = new
                        ObjectInputStream(inFile); 
                        c = (Car)in.readObject(); 
                        in.close(); 
                        inFile.close(); 
                    } 
                    catch(IOException e) 
                    { 
                         System.err.println("ERROR"); 
                    } 
                    catch(ClassNotFoundException e) 

                    { 
                         System.err.println("ERROR"); 
                    } 
                    System.out.println(c.toString()); 
                } 

            } 

现在是我们真相的时刻。让我们执行主方法。当我们在控制台中运行我们的文件时,我们得到的输出是一个 PIN 码:2000 Green Nisan Altima with vin: FDAJFD54254。如下面的截图所示:

这是我们在Serialize.java类的main方法中声明并序列化到文件中的同一辆车。显然,我们取得了成功。对象的序列化是 Java 非常优雅和出色的功能之一。

总结

在本章中,我们经历了编写和读取数据文件的过程,我们看到了FileWriterFileReader类的用法,以及如何使用close()方法释放资源。我们还学习了如何捕获异常并处理它。然后,您学习了如何使用BufferedWriterBufferedReader类分别包装FileWriterFileReader类。最后,我们看到了 I/O 的另一个方面:Serializable类。我们分析了序列化的含义以及在序列化和反序列化对象方面的用法。

在下一章中,您将学习基本的 GUI 开发。

第十章:基本 GUI 开发

有时,我们编写的程序完全关乎原始功能。然而,我们经常编写的程序通常由我们或其他用户使用,他们期望与我们互动的过程变得流畅。在本章中,我们将看到 NetBeans 中图形用户界面GUI)的基本功能。真正了不起的软件程序的几个定义是它们的 GUI 和用户体验。您将学习如何使用JFrame类创建应用程序窗口,设置其大小,向其添加标签,并关闭整个应用程序。然后是 GUI 编辑器的主题,即调色板;在这里,我们将看到调色板的工作实例以及其中可用的组件。最后,您将学习如何通过添加按钮并向其添加功能来触发事件。

本章我们将涵盖以下主题:

  • Swing GUI

  • 可视化 GUI 编辑工具 - 调色板

  • 事件处理

Swing GUI

NetBeans 是一个功能强大的程序,提供了许多功能,我们通过 NetBeans 提供的 GUI、菜单和按钮来访问这些功能。理论上,我们可以选择将 NetBeans 作为一个命令行程序来操作,但是为了像那样使用 NetBeans,我们将不得不记住或查找一个大型的特定命令库,以执行我们想要执行的每个操作。一个功能强大且写得很好的应用程序具有流畅的界面,将引导我们进入重要的功能,并使我们轻松访问它。JDK 包含一个 Java 扩展库,即swing库,它使我们能够非常容易地将我们自己的代码包装在像 NetBeans 这样的 GUI 中。

JFrame 类

为了开始这个跟踪,我们将编写一个程序,将打开一个新的 GUI 窗口。步骤如下:

  1. swing Java GUI 中心是JFrame类。在我们的情况下,这个类将是我们的操作系统处理的实际窗口对象,我们可以在屏幕上移动它。我们可以创建一个新的JFrame类,就像创建任何其他对象一样。我们甚至可以向这个JFrame类的创建传递一些参数。如果我们只给它一个字符串参数,我们将告诉JFrame类要将什么作为它的名称呈现出来:
        package GUI; 
        import javax.swing.*; 

        public class GUI { 
            public static void main(String[] args) { 
                JFrame frame = new JFrame("Hello World GUI"); 
            } 

        } 
  1. 一旦我们声明了JFrame类,它就会像任何其他对象一样存在于 Java 的内存中。除非我们明确告诉它,否则它不会呈现给用户。它只是一个对setVisible函数的函数调用,我们将为这个函数分配值true,非常简单对吧:
        frame.setVisible(true); 
  1. 在我们使 JFrame 窗口可见之前,我们还应该调用pack方法:
        frame.pack(); 

当我们创建更复杂的框架时,它们可能包含大量信息,在 GUI 中,这些信息占据了可见空间。pack方法基本上预先构建了框架中对象之间的物理关系,并确保当它实际对用户可见时,框架不会表现出奇怪的行为。到目前为止,我们已经编写了一个非常简单的程序 - 只有三行代码,我们不需要考虑太多:

        package gui; 
        import javax.swing.*; 

        public class GUI { 

            public static void main(String[] args) { 
               JFrame frame = new JFrame("Hello World GUI"); 
               frame.pack(); 
               frame.setVisible(true); 
            } 

        } 

当我们运行这个程序时,可能会看起来什么都没有发生,但实际上是有的。在屏幕的左上角,出现了一个新窗口。如果我们点击这个窗口的右侧,理论上我们可以拖动它或者调整窗口的大小:

这是一个完全成熟的窗口,我们的操作系统现在可以处理,允许我们移动; 它甚至支持动态调整大小。您会看到我们的标题也已附加到我们的窗口上。所以这非常基础。

设置窗口的大小

现在让我们看看我们的现有JFrame类还能做些什么。当我们的 JFrame 窗口出现时,它非常小而且很难看到。这样大小的程序窗口永远不会对任何人有用,所以让我们看看frame在设置窗口大小方面给我们的能力。通常,我们会使用setPreferredSize方法来为我们的JFrame类应用大小。还有一个setSize方法,但是这个方法并不总是给我们期望的结果。这是因为现在我们的JFrame类被设置为可调整大小,我们不应该明确地为它分配一个大小;相反,我们应该指示它在没有用户的其他输入的情况下,即调整 JFrame 窗口大小,窗口应该是某个大小。

我们可以使用Dimension类来存储、操作和创建大小信息。要构造一个新的维度,我们只需给它一个宽度和高度。所以让我们将JFrame类的首选大小,即在拉伸之前它想要的大小,设置为400 x 400

frame.setPreferredSize(new Dimension(400, 400)); 

Dimension类位于另一个库中,所以我们需要导入java.awt.*;包,然后我们应该能够构建和编译我们的项目,然后再次打开我们的新 GUI:

现在我们有一个不错的正方形 GUI 来开始;但是,因为里面没有任何内容,它仍然相当无用。

添加一个标签

现在让我们从编程角度快速看一下如何向我们的 GUI 添加元素。可能我们可以放在JFrame中的最简单的元素是JLabel。标签负责包含文本,实例化它们非常简单。我们只需告诉它们应该包含什么文本。当然,在更复杂的程序和 GUI 中,这个文本可能会变得动态并且可能会改变,但现在,让我们只是显示一些文本:

JLabel label = new JLabel("Hi. I am a GUI."); 

仅仅声明我们有一个JLabel类是不够的。我们还没有以任何方式将这个标签对象与我们现有的窗口关联起来。我们的窗口,你可能可以通过它公开的大量方法和成员来看出来,有很多组件,我们需要知道我们需要将我们的新JLabel类放在这些组件中的哪一个:

package gui; 
import javax.swing.*; 
import java.awt.*; 
public class GUI { 

    public static void main(String[] args) { 
        JFrame frame = new JFrame("Hello World GUI"); 
        frame.setPreferredSize(new Dimension(400, 400)); 
        JLabel label = new JLabel("Hi. I am a GUI."); 

        frame.pack(); 
        frame.setVisible(true); 
    } 

} 

在我们的JFrame类中的一个组件是contentPane;那是我们在窗口内可见的区域,通常程序的 GUI 中的东西都放在那里。这似乎是我们添加新组件的一个合理位置,在这种情况下是label。再一次,让我们构建我们的程序,关闭旧实例,然后运行新程序:

现在我们的 GUI 中有文本了!我们成功地向我们的 JFrame 窗口的内容中添加了一个元素。

关闭我们的应用程序

有点烦人的是,我们的程序在关闭关联的 GUI 后仍在继续运行。这有点傻。当我在 NetBeans GUI 上按关闭按钮时,NetBeans 关闭自身,并在我的系统上停止运行作为一个进程。我们可以使用它的setDefaultCloseOperation方法指示我们的窗口终止关联的进程。这个方法的返回类型是void,并且以整数值作为参数。这个整数是一个枚举器,有很多选项可供我们选择。所有这些选项都是由JFrame类静态声明的,我们可能正在寻找的是EXIT_ON_CLOSE,当我们关闭窗口时,它将退出我们的应用程序。构建和运行程序,终止 GUI,我们的进程也随之结束,不再悄悄地在后台运行:

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 

这是我们对 Java 中 GUI 的基本介绍。创建 GUI 很复杂,但也很令人兴奋,因为它是视觉和即时的;而且它真的很强大。

如下面的代码块所示,我们的程序现在是功能性的,但如果我们要扩展它,我们可能最终会遇到一些非常奇怪和令人困惑的问题。我们现在所做的与创建新 GUI 时的推荐做法相悖。这些推荐做法是为了保护我们免受程序变得多线程时可能出现的一些非常低级的问题。

当我们说我们的程序是多线程的时,这是什么意思?嗯,当我们创建我们的 GUI,当我们使它出现时,我们的程序从执行单个任务,即简单地从头到尾执行main方法,变成执行多个任务。这是因为我们现在正在执行以下代码:

package gui; 
import javax.swing.*; 
import java.awt.*; 
public class GUI { 

    public static void main(String[] args) { 
        JFrame frame = new JFrame("Hello World GUI"); 
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        frame.setPreferredSize(new Dimension(400, 400)); 
        JLabel label = new JLabel("Hi. I am a GUI."); 
        frame.getContentPane().add(label); 
        frame.pack(); 
        frame.setVisible(true); 
    } 

} 

然而,此外,该代码还管理了我们创建的新窗口以及该窗口执行的任何功能。为了保护自己免受多线程代码的复杂性,建议我们通过允许 Swing 实用程序异步地为我们构建此 GUI 来创建我们的新 Swing GUI。

为了实现这一点,我们实际上需要将我们写的所有代码从main方法中提取出来,放在一个地方,我们可以从main方法中引用它。这将是一个新的函数,如下面的代码行所示:

private static void MakeGUI() 

我们可以把所有这些代码都粘贴回我们的新函数中:

private static void MakeGUI() 
{ 
    JFrame frame = new JFrame("Hello World GUI"); 
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
    frame.setPreferredSize(new Dimension(400, 400)); 
    JLabel label = new JLabel("Hi. I am a GUI."); 
    frame.getContentPane().add(label); 
    frame.pack(); 
    frame.setVisible(true); 
} 

SwingUtilities 类

现在,让我们看看 Swing 建议我们如何使我们的 GUI 出现。正如我所说,swing包为我们提供了一些功能,可以为我们执行这么多的工作和思考。SwingUtilities类有一个静态的invokeLater方法,当没有其他线程真正需要被处理或者所有其他思考都做完一会儿时,它将创建我们的 GUI:

SwingUtilities.invokeLater(null); 

这个invokeLater方法希望我们向它传递一个Runnable对象,所以我们将不得不为自己创建一个Runnable对象:

Runnable GUITask = new Runnable() 

Runnable对象是可以转换为自己的线程的对象。它们有一个我们将要重写的方法,叫做runSwingUtilities.invokeLater方法将在适当时调用Runnablerun方法。当这发生时,我们希望它只是调用我们的MakeGUI方法并开始执行我们刚刚测试过的代码,那个将创建 GUI 的代码。我们将添加Override注释以成为良好的 Java 程序员,并将我们的新Runnable对象传递给SwingUtilitiesinvokeLater方法:

public static void main(String[] args) { 
    Runnable GUITask = new Runnable(){ 
        @Override 
        public void run(){ 
            MakeGUI(); 
        } 
    }; 
    SwingUtilities.invokeLater(GUITask); 
} 

运行上述程序,我们成功了!功能完全相同,我们所做的可能对于这么小的程序来说有点过度;然而,对于我们来说,看看我们在一个更大的软件项目中应该期望看到的东西是非常有益的,比如多线程可能会成为一个问题。我们走得有点快,所以让我们停下来再看一下这一部分:

Runnable GUITask = new Runnable(){ 
    @Override 
    public void run(){ 
        MakeGUI(); 
    } 
}; 

在这段代码中,我们创建了一个匿名类。虽然看起来我们创建了一个新的Runnable对象,但实际上我们创建了Runnable对象的一个新子类,它有自己特殊的重写版本的run方法,并且我们把它放在了我们的代码中间。这是一种强大的方法,可以减少所需的代码量。当然,如果我们过度使用它,我们的代码很快就会变得非常复杂,对我们或其他程序员来说很难阅读和理解。

一个可视化 GUI 编辑器工具 - 调色板

Java 编程语言,GUI 扩展库如Swing,以及一个强大的开发环境 - 如 NetBeans - 可以是一个非常强大的组合。现在我们将看看如何使用 NetBeans 中的 GUI 编辑器来创建 GUI。

为了跟上,我强烈建议您在本节中使用 NetBeans IDE。

因此,让我们开始创建一个 Java 应用程序,就像我们通常做的那样,并给它一个名称,然后我们就可以开始了。我们将从简单地删除 NetBeans 提供的默认 Java 文件,而是要求 NetBeans 创建一个新文件。我们要求它为我们创建一个 JFrame 表单:

我们将为这个 JFrame 表单命名,并将其保留在同一个包中。当 NetBeans 创建此文件时,即使它是一个.java文件,弹出的窗口对我们来说看起来会非常不同。事实上,我们的文件仍然只是 Java 代码。单击“源”选项卡,以查看代码,如下图所示:

调色板的工作原理

我们可以在“源代码”选项卡中看到组成我们文件的 Java 代码;如果我们展开它,这个文件中实际上有很多代码。这些代码都是由 NetBeans 的调色板 GUI 编辑器为我们生成的,如下图所示。我们对这个 Java 文件所做的更改将影响我们的“设计”文件,反之亦然。从这个“设计”文件中,我们可以访问拖放编辑器,并且还可以编辑单个元素的属性,而无需跳转到我们的 Java 代码,也就是“源”文件。最终,在我们创建的任何应用程序中,我们都将不得不进入我们的 Java 代码,以为我们放入编辑器的部分提供后端编程功能;现在,让我们快速看一下编辑器的工作原理。

我想为密码保护对话框设置框架。这不会太复杂,所以我们将使 JFrame 表单比现在小一点。然后,看一下可用的 Swing 控件;有很多。事实上,借助 NetBeans,我们还可以使用其他 GUI 扩展系统:

以下是设置密码保护对话框框架的步骤:

  1. 让我们只使用 Swing 控件,保持相当基本。标签是最基本的。我们的密码对话框需要一些文本:

  2. 现在,密码对话框还需要一些用户交互。我们不仅需要密码,还需要用户的用户名。要获取用户名,我们将不得不在 Swing 控件下选择几个选项:

文本区是一个好选择。它允许用户在框中输入文本,不像标签,只有开发人员可以编辑。用户可以点击框并在其中输入一些文本。不幸的是,这个框相当大,如果我们点击它并尝试使其变小,我们将得到滚动条,以允许用户在其大小周围移动。

当滚动条出现时,我们可以通过更改我们可以从编辑器访问的任意数量的属性来修改此框的默认大小。然而,一个更简单的解决方案是简单地使用文本字段,它没有我们的框的所有多行功能。此外,在文本字段旁边放置标签,您会注意到图形编辑器有助于对齐事物。如果我们正确地双击诸如标签之类的字段,我们可以在那里编辑它们的文本:

现代 GUI 的一个很酷的功能是有一些非常专业化的控件。其中之一是密码字段。在许多方面,它将表现得就像我们的文本字段控件一样,只是它会用点来替换用户在其中输入的任何文本,以便正在旁边看的人无法学到他们的密码。如果您未能双击可编辑元素,它将带您返回到源代码。

我们将编辑两个组件 - 文本和密码字段 - 以便我们的用户可以在其中放置文本,以便它们最初不会显示为默认值。我们可以双击密码字段,或者只需编辑控件的属性:

在这里,我们的文本字段控件的文本值可以被修改为一开始什么都没有,我们的密码也可以做同样的事情。您会注意到我们的密码的文本值实际上有文本,但它只显示为一堆点。但是,程序员可以访问此值以验证用户的密码。在属性选项卡中还有很多其他选项:我们可以做诸如更改字体和前景和背景颜色,给它边框等等的事情。

当我们运行程序时,您将看到它实际上存在,并且用户可以将值放入这些字段中:

当然,我们还没有编写任何后端代码来对它们进行有用的操作,但 GUI 本身已经可以运行。这里没有发生任何魔法。如果我们跳转到这段代码的源代码并转到其main方法,我们将看到实际创建和显示给用户的 GUI 的代码(请参见以下屏幕截图):

重要的是要意识到,当我们访问源代码中的元素时,所有这些方法论也可以通过原始 Java 提供给我们。这就是我在这一部分真正想向您展示的内容,只是原始的力量以及我们如何快速使用 NetBeans 图形编辑器为系统设置 GUI 窗口。

事件处理

在 Java 中工作的最好的事情之一是它的 GUI 扩展库有多么强大,以及我们可以多快地让一个程序运行起来,不仅具有功能代码,而且还有一个漂亮的专业外观的用户界面,可以帮助任何人与我们的程序交互。这就是我们现在要做的:将基本用户名和密码验证的设计界面与我们将要编写的一些后端代码连接起来,这些代码实际上将检查两个文本字段,看它们是否是我们正在寻找的。

首先,我们有一个基本的 GUI,其中有一个文本字段,用户可以在其中放置用户名和密码,这些将显示为星号:

添加按钮

到目前为止,此 GUI 的源代码完全是自动生成的。我们还没有触及它;它只是反映了我们在这里做出的设计决策。在我们开始编写后端代码来验证用户名和密码之前,我们的用户需要一种方式告诉我们,他们已经输入了用户名和密码,并且希望对其进行验证。这似乎是一个适合全能按钮的工作。因此,让我们从 Swing Controls 菜单中向我们的 GUI 添加一个按钮。我们将在属性选项中将其文本更改为提交,用户需要单击此按钮以提交他们的信息。现在,当单击按钮时,我们希望它执行一些编程逻辑。我们要检查用户名和密码字段,因为我们只是在学习和简单易行的事情;我们将只检查它们是否与一些硬编码的文本匹配。

问题是我们如何从 GUI 到功能性的 Java 代码?一般来说,我们将通过事件驱动编程模式来实现这一点,用户与 GUI 的交互决定了执行哪些 Java 代码以及发生了什么后端逻辑。另一种思考方式是,我们可以设置我们的 Java 代码的片段或方法来监听特定的与 GUI 相关的事件,并在它们发生时执行。您会注意到我们的 GUI 组件或控件,比如我们的按钮,其属性下有一个名为事件的字段。这些都是与我们的控件相关的可能发生的事情。理论上,我们可以将这些事件中的每一个绑定到我们 Java 源代码中的一个方法,当特定事件发生时,无论是因为用户交互还是我们编写的其他代码,我们相关的 Java 方法都会被调用。

为我们的按钮添加功能

为了让用户点击我们的按钮字段并执行一些编码操作,我们将为我们的actionPerformed事件分配一个事件处理程序。如果我们点击这个字段,我们已经有一个选项。我们的 GUI 设计师建议我们添加一个处理程序,即jButton1ActionPerformed。这是一个糟糕的方法名称,它将在我们的代码中存在;jBbutton1相当不具描述性。然而,它被选择是因为它是在实际的 Java 代码中创建jButton时分配的变量名:

// Variables declaration - do not modify 
private javax.swing.JButton jButton1; 
private javax.swing.JLabel jLabel1; 
private javax.swing.JLabel jLabel2; 
private javax.swing.JLabel jLabel3; 
private javax.swing.JPasswordField jPasswordField1; 
private javax.swing.JTextField jTextField1; 
// End of variables declaration 

如果我们在源代码中向下滚动,我们会看到实际的声明。我相信我们可以更改这些设置,但 NetBeans 会让我们知道我们可能不应该直接修改这个。这是因为设计师也将对其进行修改。所以我们只需将按钮的名称从不具描述性的jButton1更改为SubmitButton

// Variables declaration - do not modify 
private javax.swing.JButton SubmitButton; 

当我们进行这个更改时,我们会看到 NetBeans 会更新我们的源代码,有一个SubmitButton对象在那里跳来跳去。这是一个以大写字母开头的变量,所以我们将在事件部分进行一次更改,将其更改为submitButton

现在 NetBeans 建议执行的操作是submitButtonActionPerformed。当我们转到源代码时,我们会看到一个事件已经被创建,并且链接到了一个巨大的生成代码块中的jButton,这是 NetBeans 为了模仿我们通过他们的工具创建的 GUI 而创建的。如果我们在源代码中搜索我们的submitButtonActionPerformed方法,我们实际上会看到它被添加到生成的代码中:

public void actionPerformed(java.awt.event.ActionEvent evt) { 
    submitButtonActionPerformed(evt); 
} 

我们的submitButtonActionPerformed方法已被添加为submitButton中放置的ActionListener的最终调用:

submitButton.addActionListener(new java.awt.event.ActionListener() { 
    public void actionPerformed(java.awt.event.ActionEvent evt) { 
        submitButtonActionPerformed(evt); 
    } 
}); 

ActionListener当然只有一个工作,那就是看我们的按钮是否被点击。如果被点击,它将调用我们的submitButtonActionPerformed方法。因此,在这个submitButtonActionPerformed方法中,我们可以放一些老式的功能性 Java 代码。为此,我们需要做两件事:

  • 检查密码字段的值

  • 检查用户名字段的值

只有ActionEvent(如前面的代码块中所示)被传递到我们的submitButtonActionPerformed方法中。虽然与这个事件相关联的有很多有趣和有用的方法,但是导致我们的方法被调用的动作的上下文,它不会给我们我们真正需要的东西。我们真正需要的是我们的密码字段和我们的文本字段,幸运的是它们是我们当前类的私有成员。验证我们文本字段的值的步骤如下:

  1. 从用户名开始,也就是jTextField1
        private void submitButtonActionPerformed
        (java.awt.event.ActionEvent evt) { 
            jTextField1 
        } 

当我们有机会时,我们应该重命名它,但现在我们只能接受它,因为我们只有一个文本字段:

如果您记得,在属性选项卡下的编辑器中,这个文本字段有一个文本属性。我们去掉了这个文本,因为我们不希望我们的用户名文本字段以任何文本开头。我们希望它是空白的,这样用户就会知道他们必须在那里放入自己的信息。

  1. 现在,如果这是设计师向我们公开的属性,那么对象本身应该有一个相关的属性,确实有,即getText()
        private void submitButtonActionPerformed
        (java.awt.event.ActionEvent evt) { 
            jTextField1.getText() 
        } 
  1. 当我们调用getText时,当然,我们返回当前存储在文本字段中的文本,并且我们将我们的超级秘密用户名设置为非常“有创意”的单词username

这是一个条件语句,我们将要做另一个条件语句。我们想要询问我们的程序文本字段和密码字段 - 在这种情况下将暴露一个类似的方法getPassword - 是否都等于硬编码的字符串。我们的秘密密码将是java。请注意,getPassword实际上返回一个字符数组,而不是一个字符串,所以为了保持简单,让我们将密码值分配给一个字符串,然后我们就可以将其用作字符串。在我们的条件语句前面加上if,在括号内,我们就可以开始了:

            private void submitButtonActionPerformed
            (java.awt.event.ActionEvent evt) { 
                String password = new
                String(jPasswordField1.getPassword()); 
                if (jTextField1.getText().equals("username")
                && password.equals("java")) 
                { 

                } 
            } 

现在我们需要给我们的用户一些指示,无论他们是否成功提供了正确的用户名和密码。好的,如果用户成功输入了一个好的用户名和一个好的密码,我们该怎么办呢?嗯,我认为如果我们在这里显示一个弹出对话框会很酷。

  1. JOptionPane为我们提供了showMessageDialog方法,这是一种非常酷的方式,可以向用户传达非常重要和即时的信息。它会显示一个弹出框,非常轻量级且易于使用。您可能需要修复这个导入:
        { 
            JOptionPane.showMessageDialog(rootPane, password); 
        } 

MessageDialog需要创建自己的唯一重量级信息是要附加到的 GUI 组件,作为其父级。我们可以通过ActionEvent获取button evt,但这并没有太多意义,因为对话框不仅仅与按钮绑定;它与这个 GUI 的整体相关,这是验证用户名和密码。因此,如果我们可以将消息对话框绑定到 JFrame 表单本身,GUI 的顶级元素,那将是很好的,实际上我们可以:

            public class MyGUI extends javax.swing.JFrame { 

                /** 
                 * Creates new form MyGUI 
                */ 
                public MyGUI() { 
                    initComponents(); 
                } 
  1. 如果我们向上滚动一点到我们的源代码部分,检查我们正在写代码的确切位置,我们会看到我们在一个名为MyGUI的类中,它扩展了JFrame类。整个类与我们正在使用的JFrame类相关联。因此,要将JFrame作为变量传递给我们的showMessageDialog方法,我们只需使用this关键字。现在只需输入一条消息,以便在验证密码和用户名时向用户显示:
        private void submitButtonActionPerformed
        (java.awt.event.ActionEvent evt) { 
            String password = new String(jPasswordField1.getPassword()); 
            if (jTextField1.getText().equals("username") 
            && password.equals("java")) 
            { 
                 JOptionPane.showMessageDialog(this, "Login Good!"); 
            } 
        } 

让我们运行我们的程序,看看我们建立了什么。对话框出现了,这是我们之前见过的,也是我们期望的,然后执行以下步骤:

1. 输入我们的有效用户名,即username

2. 输入我们的有效密码,即java

3. 然后,点击提交按钮。

我们得到一个对话框,看起来像下面的截图。我们可以自由地在我们的 JFrame 实例中移动这个框:

只是为了测试一下,让我们输入一些胡言乱语。无论我们点击多少次提交,我们都得不到任何东西。而且一个好的用户名和没有密码也得不到任何东西,非常酷!我们只是触及了 Java GUI 可能性的表面,当然,也是 Java 本身。

为我们的程序创建 Java GUI 是容易的,在许多情况下,也是无痛的。有时,GUI 强制我们实现事件处理模型,在某种程度上甚至可以使我们创建依赖用户交互的 Java 程序变得更容易。

另一个我无法再强调的重要事情是,尽管 GUI 设计师很棒,我们也可以通过简单地坐下来在源代码部分编写 Java 代码来创建完全相同的项目。

我并不是说我们不应该使用 GUI 设计师 - 尤其是因为有很多代码和很多由 GUI 设计师为我们生成的精心编写的代码,这可以节省我们大量的时间 - 但这里绝对没有任何魔法发生。这都是使用Swing扩展库的 Java 代码。

总结

在本章中,我们看到了 NetBeans 中 GUI 的基本功能。您学会了如何使用JFrame类创建应用程序窗口,设置其大小,向其添加标签,并关闭应用程序。然后,我们深入讨论了 GUI 编辑器,调色板的主题。我们看到了一个工作的调色板以及其中可用的组件。最后,您学会了如何通过添加按钮并为其添加功能来触发事件。

在下一章中,您将学习有关 XML 的知识。

第十一章:XML

假设我们想要存储具有对我们程序有意义的结构的信息。此外,我们希望这些信息在某种程度上是可读的,有时甚至是可编辑的。为了实现这一点,我们经常转向 XML。

Java 为我们提供了强大的工具,用于操作、读取和编写 XML 原始文本和文件。然而,与许多强大的工具一样,我们需要学习如何使用它们。在本章中,我们首先将看看如何使用 Java 将 XML 文件加载到 Java 对象中。接下来,我们将逐步介绍如何使用 Java 解析 XML 数据。最后,我们将看到用于编写和修改 XML 数据的 Java 代码。

在本章中,我们将涵盖以下主题:

  • 用于读取 XML 数据的 Java 代码

  • 解析 XML 数据

  • 编写和修改 XML 数据

读取 XML 数据

在本节中,我们将完成一个非常简单的任务,以便开始学习 Java 如何与 XML 交互的道路。我们将使用代码文件中提供的cars.xml文件中的 XML 信息。这个文件应该存储在我们 Java 项目的当前目录中,所以当我们运行我们的 Java 程序时,它将能够访问cars.xml而不需要任何额外的路径。我们将编辑以下 Java 程序以加载cars.xml文件:

package loadinganxmlfile; 

import java.io.*; 
import javax.xml.parsers.*; 
import javax.xml.transform.*; 
import javax.xml.transform.dom.*; 
import javax.xml.transform.stream.*; 
import org.w3c.dom.*; 
import org.xml.sax.*; 

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

        try { 
            //Write code that can throw errors here 
        } 
        catch (ParserConfigurationException pce) { 
            System.out.println(pce.getMessage()); 
        } 
        catch (SAXException se) { 
            System.out.println(se.getMessage()); 
        } 
        catch (IOException ioe) { 
            System.err.println(ioe.getMessage()); 
        } 
    } 

    private static void PrintXmlDocument(Document xml) 
    { 
        try{ 
            Transformer transformer = 
             TransformerFactory.newInstance().newTransformer(); 
            StreamResult result = new StreamResult
             (new StringWriter()); 
            DOMSource source = new DOMSource(xml); 
            transformer.transform(source, result); 
            System.out.println(result.getWriter().toString()); 
        } 
        catch(TransformerConfigurationException e) 
        { 
            System.err.println("XML Printing Failed"); 
        } 
        catch(TransformerException e) 
        { 
            System.err.println("XML Printing Failed"); 
        } 
    } 
} 

在我们开始之前,请注意这个程序需要大量的导入。我们导入的transform类对我们将要编写的任何内容都不是必需的;我已经编写了一个名为PrintXmlDocument()的函数,如果我们成功加载它,它将把我们的 XML 文档打印到控制台窗口。如果您在本节中跟着代码,我建议您首先从一开始导入这些transform类。然后,当您使用额外的功能时,继续使用 NetBeans 的“修复导入”功能,以确切地看到工具所使用的库来自哪里。

让我们开始吧。我们的最终目标是拥有一个Document类的对象,其中包含我们cars.xml文件中的信息。一旦我们有了这个Document对象,我们只需要调用Document实例上的PrintXmlDocument()函数,就可以在我们的控制台窗口中看到信息。

不幸的是,创建这个Document对象并不像说Document dom = new Document();那样简单。相反,我们需要以一种结构化和程序化的方式创建它,以正确地保留我们的 XML 文件的可解析性。为此,我们将使用另外两个类:DocumentBuilderDocumentBuilderFactory类。

DocumentBuilder类,信不信由你,将负责实际为我们构建文档。DocumentBuilder类作为一个独立的实体存在,与Document对象分开,这样我们作为程序员可以在逻辑上分开我们可以对文档本身执行的方法和创建该文档所需的附加方法的范围。与Document类类似,我们不能只是实例化DocumentBuilder类。相反,有一个第三个类我们将利用来获取DocumentBuilder,即DocumentBuilderFactory类。我已经将创建Document对象所需的代码分为三部分:

  1. DocumentBuilderFactory类包含一个名为newInstance()的静态方法。让我们在main()方法的第一个try块中添加以下方法调用。这将为我们实例化DocumentBuilderFactory以便我们可以使用它:
        DocumentBuilderFactory factory = 
        DocumentBuilderFactory.newInstance(); 
  1. 一旦我们有了DocumentBuilderFactory,我们就可以为自己获取一个新的DocumentBuilder对象。为此,我们将调用工厂的newDocumentBuilder()方法。让我们把它添加到我们的 try 块中:
        DocumentBuilder builder = factory.newDocumentBuilder(); 
  1. 最后,我们需要指示DocumentBuilder构建一个Document对象,并且该对象应该反映我们的cars.xml文件的结构。我们将在我们的try块中简单地用一个值实例化我们的Document对象。我们将从builderparse()方法中获取这个值。这个方法的一个参数是一个引用文件名的字符串。如果我们在我们的 Java 程序中有一个引用文件对象,我们也可以使用它:
        Document dom = builder.parse("cars.xml"); 

现在我们的main()方法看起来如下:

        public static void main(String[] args) { 
            DocumentBuilderFactory factory = 
            DocumentBuilderFactory.newInstance(); 
            try { 
                // Write code that can throw errors here... 
                DocumentBuilder builder = 
                factory.newDocumentBuilder(); 
                Document dom = builder.parse("cars.xml"); 

                PrintXmlDocument(dom); 
            } 
            catch (ParserConfigurationException pce) { 
                System.out.println(pce.getMessage()); 
            }  
            catch (SAXException se) { 
                System.out.println(se.getMessage()); 
            } 
            catch (IOException ioe) { 
                System.err.println(ioe.getMessage()); 
            } 
        } 

现在是时候检查我们的代码是否有效了。我们使用DocumentBuilderFactory类的静态方法获取了DocumentBuilderFactory对象,并创建了一个全新的实例。通过DocumentBuilderFactory,我们创建了一个新的DocumentBuilder对象,它将能够智能地解析我们的 XML 文件。在解析我们的 XML 文件时,DocumentBuilder对象了解其中包含的信息的性质,并能够将其存储在我们的 XML 文档或Document对象模型元素中。当我们运行这个程序时,我们会得到原始 XML 文档的文本视图作为输出:

由于加载这样的 XML 文件有很多步骤,我想将其放在自己的部分中。这样,当我们作为程序员学习如何从 XML 中操作和读取有价值的信息时,我们不会被我们在这里看到的所有语法所困扰。

解析 XML 数据

Document类为我们提供了一种简单的方法来在对象中存储格式化的信息。在前面部分的程序中,我们从cars.xml文件中读取信息到我们的 Java Document对象中。cars.xml文件如下所示:

<?xml version="1.0"?> 
<cars> 
    <owner name="Billy"> 
        <car vin="LJPCBLCX11000237"> 
            <make>Ford</make> 
            <model>Fusion</model> 
            <year>2014</year> 
            <color>Blue</color> 
        </car> 
        <car vin="LGHIALCX89880011"> 
            <make>Toyota</make> 
            <model>Tacoma</model> 
            <year>2013</year> 
            <color>Green</color> 
        </car> 
        <car vin="GJSIALSS22000567"> 
            <make>Dodge</make> 
            <model>Charger</model> 
            <year>2013</year> 
            <color>Red</color> 
        </car> 
    </owner> 
    <owner name="Jane"> 
        <car vin="LLOKAJSS55548563"> 
            <make>Nissan</make> 
            <model>Altima</model> 
            <year>2000</year> 
            <color>Green</color> 
        </car> 
        <car vin="OOKINAFS98111001"> 
            <make>Dodge</make> 
            <model>Challenger</model> 
            <year>2013</year> 
            <color>Red</color> 
        </car> 
    </owner> 
</cars> 

这个文件的根节点是cars节点,这个节点中包含两个owner节点,即 Billy 和 Jane,每个节点中都有一些car节点。这些car元素中存储的信息与我们之前的 Java 类中可以存储的信息相对应。

本节的目标是从cars.xml中获取特定所有者(在本例中是 Jane)的汽车信息,并将这些信息存储在我们自定义的Car类中,以便我们可以利用Car类的toString()重写以以良好格式的方式将 Jane 的所有汽车打印到我们的控制台上。

通过我们已经设置的代码,我们的Document对象dom以相同的格式反映了cars.xml中存储的信息,所以我们只需要弄清楚如何询问这个Document对象这个问题:Jane 拥有什么车?为了弄清楚如何编写代码,你需要了解一些关于 XML 术语的知识。在本节中,我们将处理术语“元素”和“节点”。

在 XML 中,元素是一个具有开始和结束标记的实体,它还包含其中的所有信息。当我们的Document对象返回信息时,通常会以节点的形式返回信息。节点是 XML 文档的构建块,我们几乎可以将它们视为继承关系,其中所有元素都是节点,但并非所有节点都是元素。节点可以比整个 XML 元素简单得多。

访问 Jane 的 XML 元素

本节将帮助我们访问关于 Jane 拥有的汽车的信息,使用以下代码。我已将要添加到我们的main()函数中的代码分为六部分:

  1. 因此,在寻找 Jane 拥有的所有汽车的过程中,让我们看看我们的 XML 文档在一开始为我们提供了哪些功能。如果我们通过代码补全快速扫描我们的方法列表,我们可以从我们的Document实例中调用dom。我们将看到getDocumentElement()方法为我们返回一个元素:

这可能是一个很好的开始方式。这个方法返回我们的 XML 中的顶层元素;在这种情况下,我们将得到cars元素,其中包含了我们需要的所有信息。它还包含一些我们不需要的信息,比如 Billy 的车,但在我们访问之后我们会解析出来。一旦我们导入了正确的库,我们就可以直接在我们的代码中引用 XML 元素的概念,使用Element类。我们可以创建一个新的Element对象,并将其值分配给我们的 XML 文档的根元素:

        Element doc = dom.getDocumentElement(); 

当然,我们需要更深入。我们的 XML 文档cars的根级别对我们来说并不直接有用;我们需要其中包含的信息。我们只真正想要一个owner节点的信息(包含关于 Jane 的车的信息)。但由于 XML 解析的方式,我们可能最好先获取这两个所有者节点,然后找到我们真正感兴趣的那个。

为了获取这两个节点,我们可以在我们刚刚创建并存储在doc中的根 XML 元素上调用一个方法。XML 元素可以包含其中的其他元素;在这种情况下,我们的根元素包含了许多owner元素。getElementsByTagName()方法允许我们收集这些内部元素的数量。XML 元素的标签名就是你所期望的;它是我们给我们的 XML 的特定元素的名称。在这种情况下,如果我们要求在我们文档的根元素中包含的所有标签名为owner的元素,我们将进一步缩小我们正在处理的 XML 的数量,接近我们所需的小节。

getElementsByTagName()方法返回的不是单个元素。即使在最高级别的这一部分中也有两个不同的元素,即两个所有者:BillyJane。因此,getElementsByTagLineName()方法不返回单个元素;而是返回一个NodeList对象,它是 XML 节点的集合。

        NodeList ownersList = doc.getElementsByTagName("owner"); 

现在我们根本不再处理我们的根节点;我们只有它的内容。是时候真正地缩小我们的搜索范围了。我们的NodeList对象包含多个所有者,但我们只想要一个所有者,如果与该所有者相关联的属性名称恰好是Jane。为了找到这个特定的元素(如果存在的话),我们只需循环遍历NodeList,检查它包含的每个元素的属性。请注意,ownersList不是传统数组。它是一个NodeList对象,是它自己的一种对象。因此,我们不能在其上使用正常的数组语法。幸运的是,它向我们提供了模仿正常数组语法的方法。例如,getLength()方法将告诉我们ownersList中有多少个对象:

        for(int i = 0; i < ownersList.getLength(); i++) 
        { 
        } 
  1. 同样,当我们尝试创建一个新的Element对象并将该值分配给当前循环遍历的ownersList部分时,我们将无法使用数组的正常语法。不过,ownersList再次为我们提供了一个执行相同操作的方法。item()方法提供或要求一个索引作为输入。

请注意,ownersListNodeList,但是元素是节点,不是所有节点都是元素,因此我们需要在这里做出决定。我们可以检查此函数返回的对象的性质,并确保它们实际上是 XML 元素。但为了保持事情的进行,我们只是假设我们的 XML 格式正确,并且我们只是让 Java 知道item()方法返回的节点实际上是一个元素;也就是说,它有一个开始标签和一个结束标签,并且可以包含其他元素和节点:

            Element owner = (Element)ownersList.item(i); 

一旦我们成功地从所有者列表中访问了一个元素,现在是时候检查并看看这是否是我们正在寻找的所有者;因此,我们将需要一个条件语句。XML 元素向我们公开了getAttribute()方法,我们感兴趣的属性是name属性。因此,这里的代码将询问当前的owner,“你的name属性的值是多少?”如果该值等于Jane,那么我们知道我们已经访问了正确的 XML 元素。

现在在简的 XML 元素中,我们只有一些car元素。所以,再次是时候创建NodeList并用这些car元素填充它。我们现在需要在我们当前的所有者简上调用getElementByTagName()方法。如果我们使用顶层文档来调用这个函数,我们将得到文档中的所有car元素,甚至是比利的:

            if(owner.getAttribute("name").equals("Jane")) 
            { 
                NodeList carsList = 
                owner.getElementsByTagName("car"); 
  1. 这个main()方法变得有点复杂;这是我愿意在一个方法中做到的极限。我们的代码已经深入了几个层次,我们写的代码并不简单。我认为是时候将下一部分解析成自己的方法了。让我们简单地声明我们将要有一个PrintCars()方法,这个函数将接受car元素的NodeList来打印汽车节点:
        PrintCars(carsList); 

我们的main方法现在如下所示:

        public static void main(String[] args) { 
            DocumentBuilderFactory factory = 
            DocumentBuilderFactory.newInstance(); 
            try { 
                DocumentBuilder docBuilder = 
                factory.newDocumentBuilder(); 
                Document dom = docBuilder.parse("cars.xml"); 

                // Now, print out all of Jane's cars 
                Element doc = dom.getDocumentElement(); 
                NodeList ownersList = 
                doc.getElementsByTagName("owner"); 

                for(int i = 0; i < ownersList.getLength(); i++) 
                { 
                    Element owner = (Element)ownersList.item(i); 
                    if(owner.getAttribute("name").equals("Jane")) 
                    { 
                        NodeList carsList = 
                        owner.getElementsByTagName("car"); 
                        PrintCars(carsList); 
                    } 
                } 
            } 
            catch (ParserConfigurationException pce) { 
                System.out.println(pce.getMessage()); 
            }  
            catch (SAXException se) { 
                System.out.println(se.getMessage()); 
            }  
            catch (IOException ioe) { 
                System.err.println(ioe.getMessage()); 
            } 
        } 

打印简的汽车详情

现在,离开我们的main()方法,我们将定义我们的新的PrintCars()方法。我已经将PrintCars()函数的定义分成了八个部分:

  1. 因为我们在程序的入口类中,PrintCars()方法是由静态的main()方法调用的,它可能应该是一个static函数。它将只是打印到我们的控制台,所以void是一个合适的返回类型。我们已经知道它将接受汽车的NodeList作为输入:
        public static void PrintCars(NodeList cars) 
        { 
        } 
  1. 一旦我们进入了这个函数,我们知道我们可以使用car XML 元素的列表。但为了打印出每一个,我们需要循环遍历它们。我们已经在程序中循环遍历了 XML 的NodeList,所以我们将使用一些非常相似的语法。让我们看看这个新代码需要改变什么。好吧,我们不再循环遍历ownersList;我们有一个新的NodeList对象来循环遍历carsNodeList
        for(int i = 0; i < cars.getLength(); i++) 
        { 
        } 
  1. 我们知道汽车仍然是Element实例,所以我们的强制转换仍然是合适的,但我们可能想要将我们用于循环遍历每辆汽车的变量重命名为类似carNode的东西。每次我们循环遍历一辆车时,我们将创建一个新的Car对象,并将该车的 XML 中的信息存储在这个实际的 Java 对象中:
        Element carNode = (Element)cars.item(i); 
  1. 因此,除了访问car XML,让我们也声明一个Car对象,并将其实例化为一个新的Car对象:
        Car carObj = new Car(); 
  1. 现在我们将通过从carNode中读取它们来构建存储在carObj中的值。如果我们快速跳回 XML 文件并查看存储在car元素中的信息,我们将看到它存储了makemodelyearcolor作为 XML 节点。车辆识别号vin实际上是一个属性。让我们简要看一下我们的Car.java类:
        package readingxml; 

        public class Car { 
            public String vin; 
            public String make; 
            public String model; 
            public int year; 
            public String color; 
            public Car() 
            { 

            } 
            @Override 
            public String toString() 
            { 
                return String.format("%d %s %s %s, vin:%s", year, 
                color, make, model, vin); 
            } 
        } 

让我们先从简单的部分开始;所以,makemodelcolor都是存储在Car类中的字符串,它们恰好都是car元素内的节点。

回到我们的PrintCars()函数,我们已经知道如何访问元素内的节点。我们只需要再次使用carNodegetElementsByTagName()函数。如果我们获取所有标签名为color的元素,我们应该会得到一个只包含一个元素的列表,这个元素就是我们感兴趣的,告诉我们汽车颜色的元素。不幸的是,我们在这里有一个列表,所以我们不能直接操作该元素,直到我们从列表中取出它。不过,我们知道如何做到这一点。如果我们确信我们的 XML 格式正确,我们知道我们将获得一个只包含一个项目的列表。因此,如果我们获取该列表的第 0 个索引处的项目,那将是我们要找的 XML 元素。

存储在这个 XML 元素中的颜色信息不是一个属性,而是内部文本。因此,我们将查看 XML 元素公开的方法,看看是否有一个合适的方法来获取内部文本。有一个getTextContent()函数,它将给我们所有的内部文本,这些文本实际上不是 XML 元素标签的一部分。在这种情况下,它将给我们我们汽车的颜色。

获取这些信息还不够;我们需要存储它。幸运的是,carObj的所有属性都是公共的,所以我们可以在创建car对象后自由地为它们赋值。如果这些是私有字段而没有 setter,我们可能需要在构造carObj之前进行这些信息,然后通过它们传递给它希望有的构造函数。

        carObj.color = 
        carNode.getElementsByTagName("color").item(0).getTextContent(); 

我们将为makemodel做几乎完全相同的事情。我们唯一需要改变的是我们在查找元素时提供的关键字。

        carObj.make = 
        carNode.getElementsByTagName("make").item(0).getTextContent(); 
        carObj.model = 
        carNode.getElementsByTagName("model").item(0).getTextContent(); 
  1. 现在,我们可以继续使用相同的一般策略来处理我们车辆的year,但是我们应该注意,就carObj而言,year是一个整数。就我们的 XML 元素而言,year,就像其他任何东西一样,是一个TextContent字符串。幸运的是,将一个string转换为一个integer,只要它格式良好,这是一个我们在这里将做出的假设,不是太困难。我们只需要使用Integer类并调用它的parseInt()方法。这将尽力将一个字符串值转换为一个整数。我们将把它赋给carObjyear字段。
        carObj.year = 
        Integer.parseInt(carNode.getElementsByTagName
        ("year").item(0).getTextContent()); 
  1. 这样我们就只剩下一个字段了。注意carObj有一个车辆识别号字段。这个字段实际上不是一个整数;车辆识别号可以包含字母,所以这个值被存储为一个字符串。我们获取它会有一点不同,因为它不是一个内部元素,而是car元素本身的一个属性。我们再次知道如何从carNode获取属性;我们只是要获取名称为vin的属性并将其赋给carObj
         carObj.vin = carNode.getAttribute("vin"); 
  1. 完成所有这些后,我们的carObj对象应该在所有成员中具有合理的值。现在是时候使用carObj存在的原因了:重写toString()函数。对于我们循环遍历的每辆车,让我们调用carObjtoString()函数,并将结果打印到控制台上。
        System.out.println(carObj.toString()); 

我们的PrintCars()函数现在将如下所示:

public static void PrintCars(NodeList cars) 
{ 
    for(int i = 0; i < cars.getLength(); i++) 
    { 
        Element carNode = (Element)cars.item(i); 
        Car carObj = new Car(); 
        carObj.color = 
         carNode.getElementsByTagName
         ("color").item(0).getTextContent(); 
        carObj.make = 
         carNode.getElementsByTagName
         ("make").item(0).getTextContent(); 
        carObj.model = carNode.getElementsByTagName
         ("model").item(0).getTextContent(); 
        carObj.year = 
         Integer.parseInt(carNode.getElementsByTagName
         ("year").item(0).getTextContent()); 
        carObj.vin = carNode.getAttribute("vin"); 
        System.out.println(carObj.toString()); 
    } 
} 

我们应该可以编译我们的程序了。现在当我们运行它时,希望它会打印出简的所有汽车,利用carObj的重写toString()方法,来很好地格式化输出。当我们运行这个程序时,我们得到两辆汽车作为输出,如果我们去我们的 XML 并查看分配给简的汽车,我们会看到这些信息确实与存储在这些汽车中的信息相匹配。

XML 和 Java 的组合真的非常强大。XML 是人类可读的。我们可以理解它,甚至可以对其进行修改,但它也包含了非常有价值的结构化信息。这是编程语言(如 Java)也能理解的东西。我们在这里编写的程序虽然有其特点,并且需要一定的知识来编写,但比起从原始文本文件中编写类似程序,它要容易得多,程序员也更容易理解和维护。

编写 XML 数据

能够读取 XML 信息当然很好,但是为了使语言对我们真正有用,我们的 Java 程序可能也需要能够写出 XML 信息。以下程序是一个从同一 XML 文件中读取和写入的程序的基本模型:

package writingxml; 

import java.io.*; 
import javax.xml.parsers.*; 
import javax.xml.transform.*; 
import javax.xml.transform.dom.*; 
import javax.xml.transform.stream.*; 
import org.w3c.dom.*; 
import org.xml.sax.*; 

public class WritingXML { 
    public static void main(String[] args) { 
        File xmlFile = new File("cars.xml"); 
        Document dom = LoadXMLDocument(xmlFile);       
        WriteXMLDocument(dom, xmlFile); 
    } 

    private static void WriteXMLDocument
     (Document doc, File destination) 
    { 
        try{ 
            // Write doc to destination file here... 
        } 
        catch(TransformerConfigurationException e) 
        { 
            System.err.println("XML writing failed."); 
        } 
        catch(TransformerException e) 
        { 
            System.err.println("XML writing failed."); 
        } 
    } 

    private static Document LoadXMLDocument(File source) 
    { 
        try { 
            DocumentBuilderFactory factory = 
             DocumentBuilderFactory.newInstance(); 
            DocumentBuilder builder = 
             factory.newDocumentBuilder(); 
            Document dom = builder.parse(source); 
        } 
        catch (ParserConfigurationException e) { 
             System.err.println("XML loading failed."); 
        } 
        catch (SAXException e) { 
             System.err.println("XML loading failed."); 
        } 
        catch (IOException e) { 
            System.err.println("XML loading failed."); 
        } 

        return dom; 
    } 
} 

它的main()方法非常简单。它接受一个文件,然后从该文件中读取 XML,将其存储在 XML 文档的树对象中。然后,该程序调用WriteXMLDocument()将 XML 写回同一文件。目前,用于读取 XML 的方法已经为我们实现(LoadXMLDocument());然而,用于写出 XML 的方法尚未完成。让我们看看我们需要为我们写入 XML 信息到文档发生什么。我已将WriteXMLDocument()函数的代码分为四个部分。

用于编写 XML 数据的 Java 代码

编写 XML 数据需要执行以下步骤:

  1. 由于 XML 文档的存储方式,我们需要将其转换为不同的格式,然后才能真正将其以与原始 XML 相同的格式打印到文件中。为此,我们将使用一个名为Transformer的专用于 XML 的类。与处理文档模型中的许多类一样,最好使用工厂来创建Transformer实例。在这种情况下,工厂称为TransformerFactory,像许多工厂一样,它公开了newInstance()方法,允许我们在需要时创建一个。要获取我们的新Transformer对象,它将允许我们将我们的Document对象转换为可发送到文件的流的东西,我们只需调用TransformerFactorynewTransformer()方法:
        TransformerFactory tf = TransformerFactory.newInstance(); 
        Transformer transformer = tf.newTransformer(); 
  1. 现在,在Transformer可以将我们的 XML 文档转换为其他内容之前,它需要知道我们希望它将我们当前Document对象的信息转换为什么。这个类就是StreamResult类;它是存储在我们当前Document对象中的信息的目标。流是一个原始的二进制信息泵,可以发送到任意数量的目标。在这种情况下,我们的目标将是提供给StreamResult构造函数的目标文件:
        StreamResult result = new StreamResult(destination); 
  1. 我们的Transformer对象并不会自动链接到我们的 XML 文档,它希望我们以唯一的方式引用我们的 XML 文档:作为DOMSource对象。请注意,我们的source对象(接下来定义)正在与result对象配对。当我们向Transformer对象提供这两个对象时,它将知道如何将一个转换为另一个。现在,要创建我们的DOMSource对象,我们只需要传入我们的 XML 文档:
        DOMSource source = new DOMSource(doc); 
  1. 最后,当所有设置完成后,我们可以执行代码的功能部分。让我们获取我们的Transformer对象,并要求它将我们的源(即DOMSource对象)转换为一个流式结果,目标是我们的目标文件:
         transformer.transform(source, result); 

以下是我们的WriteXMLDocument()函数:

private static void WriteXMLDocument
(Document doc, File destination) 
{ 
    try{ 
        // Write doc to destination file here 
        TransformerFactory tf = 
         TransformerFactory.newInstance(); 
        Transformer transformer = tf.newTransformer(); 
        StreamResult result = new StreamResult(destination); 
        DOMSource source = new DOMSource(doc); 

        transformer.transform(source, result); 
    } 
    catch(TransformerConfigurationException e) 
    { 
        System.err.println("XML writing failed."); 
    } 
    catch(TransformerException e) 
    { 
        System.err.println("XML writing failed."); 
    } 
} 

当我们运行这个程序时,我们将在文件中得到一些 XML,但是当我说这是我们之前拥有的相同的 XML 时,你必须相信我,因为我们首先读取 XML,然后将其作为结果打印回去。

为了真正测试我们的程序是否工作,我们需要在 Java 代码中对我们的Document对象进行一些更改,然后看看我们是否可以将这些更改打印到这个文件中。让我们改变汽车所有者的名字。让我们将所有汽车的交易转移到一个名叫 Mike 的所有者名下。

修改 XML 数据

XML I/O 系统的强大之处在于在加载和写入 XML 文档之间,我们可以自由修改存储在内存中的Document对象dom。而且,我们在 Java 内存中对对象所做的更改将被写入我们的永久 XML 文件。所以让我们开始做一些更改:

  1. 我们将使用getElementsByTagName()来获取我们的 XML 文档中的所有owner元素。这将返回一个NodeList对象,我们将称之为owners
        NodeList owners = dom.getElementsByTagName("owner"); 
  1. 为了将所有这些所有者的名字转换为Mike,我们需要遍历这个列表。作为复习,我们可以通过调用ownersgetLength()函数来获取列表中的项目数,也就是我们的NodeList对象。要访问我们当前正在迭代的项目,我们将使用ownersitem()函数,并传入我们的迭代变量i来获取该索引处的项目。让我们将这个值存储在一个变量中,以便我们可以轻松使用它;再次,我们将假设我们的 XML 格式良好,并告诉 Java,事实上,我们正在处理一个完全成熟的 XML 元素。

接下来,XML 元素公开了许多允许我们修改它们的方法。其中一个元素是setAttribute()方法,这就是我们将要使用的方法。请注意,setAttribute()需要两个字符串作为输入。首先,它想知道我们想要修改哪个属性。我们将要修改name属性(这是我们这里唯一可用的属性),并且我们将把它的值赋给Mike

            for(int i = 0; i < owners.getLength(); i++) 
            { 
                Element owner = (Element)owners.item(i); 
                owner.setAttribute("name", "Mike"); 
            } 

现在我们的main()方法将如下所示:

public static void main(String[] args) { 
    File xmlFile = new File("cars.xml"); 
    Document dom = LoadXMLDocument(xmlFile); 

    NodeList owners = dom.getElementsByTagName("owner"); 
    for(int i = 0; i < owners.getLength(); i++) 
    { 
        Element owner = (Element)owners.item(i); 
        owner.setAttribute("name", "Mike"); 
    } 
    WriteXMLDocument(dom, xmlFile); 
} 

当我们运行程序并检查我们的 XML 文件时,我们将看到Mike现在是所有这些汽车的所有者,如下面的截图所示:

现在可能有意义将这两个 XML 元素合并,使Mike只是一个所有者,而不是分成两个。这有点超出了本节的范围,但这是一个有趣的问题,我鼓励你反思一下,也许现在就试一试。

总结

在本章中,我们看到了将 XML 文件读入Document对象的 Java 代码。我们还看到了如何使用 Java 解析 XML 数据。最后,我们看到了如何在 Java 中编写和修改 XML 数据。

恭喜!你现在是一个 Java 程序员。

posted @ 2025-09-12 13:57  绝不原创的飞龙  阅读(13)  评论(0)    收藏  举报