OGA-JavaSE8-程序员-1-认证指南-全-

OGA JavaSE8 程序员 1 认证指南(全)

原文:OCA Java SE 8 Programmer I Certification Guide

译者:飞龙

协议:CC BY-NC-SA 4.0

简介

本简介涵盖的内容包括

  • 介绍 Oracle 认证助理(OCA)Java SE 8 程序员 I 认证(考试编号 1Z0-808)

  • OCA Java SE 8 程序员认证的重要性

  • OCA Java SE 8 程序员 I 考试与 OCA Java SE 7 程序员 I 考试的比较

  • OCA Java SE 8 程序员 I 考试(1Z0-808)与 OCP Java SE 8 程序员 II 考试(1Z0-809)的比较

  • 详细考试目标,与书籍章节相对应

  • 关于考试准备和参加考试常见问题解答

  • 介绍用于考试的测试引擎

本书专门针对希望获得 OCA Java SE 8 程序员 I 认证(考试编号 1Z0-808)的个人。它假设你已经熟悉 Java 并有一些使用经验。如果你对 Java 编程语言一无所知,我建议你从入门级书籍开始,然后再回到这本书。

1. 免责声明

本章中的信息来源于 Oracle.com、公共网站和用户论坛。信息来自获得 Java 认证的真实人士,包括作者。已尽一切努力保持内容的准确性,但考试细节(包括考试目标、价格、考试通过分数、总题数、最大考试时间等)均受 Oracle 政策的约束。本书的作者和出版社不对因本书中包含的任何信息或因直接或间接使用此信息而造成的任何损失或损害负责。

2. OCA Java SE 8 程序员 I 认证简介

Oracle 认证助理(OCA)Java SE 8 程序员 I 考试(1Z0-808)涵盖了 Java SE 8 编程的基础知识,例如类和接口的结构、不同数据类型的变量、方法、运算符、数组、决策结构和循环。考试内容包括异常处理以及一些常用的 Java API 类,如 StringStringBuilderArrayList。本考试不包括许多 Java 8 特定的语言特性。它介绍了使用 lambda 表达式的函数式编程风格。它部分涵盖了新的日期和时间 API。

本考试是获得 Oracle 认证专业(OCP)Java SE 8 程序员头衔的两个步骤之一。它证明个人在 Java 编程语言方面拥有坚实的基础。表 1 列出了本考试的详细信息。

表 1. OCA Java SE 8 程序员 I 考试(1Z0-808)的详细信息
考试编号 1Z0-808
Java 版本 基于 Java 版本 8
题目数量 77
通过分数 65%
时间长度 150 分钟
价格 美元 300
题型 多选题

3. OCA Java SE 8 程序员 I 认证的重要性

如图 1 所示,OCA Java SE 8 程序员 I 考试 (1Z0-808) 是您的 Java 认证路线图中的入门级考试。

图 1. OCA Java SE 8 程序员认证是 Java 认证路线图中的入门级认证。

此考试是获得 OCP Java SE 8 程序员头衔的两个步骤之一。图 1 中的虚线和箭头表示认证的先决条件。OCP Java 程序员认证(任何 Java 版本)是获得 Java 中大多数其他高级认证的先决条件。

要获得 OCP Java SE 8 程序员头衔,您必须通过以下两个认证(顺序不限):

  • OCA Java SE 8 程序员 I (1Z0-808)

  • OCP Java SE 8 程序员 II (1Z0-809)

注意

在撰写本文时,Oracle 将此考试作为通过 1Z0-809 考试的先决条件。之前,Oracle 允许以任何顺序通过 1Z0-808 和 1Z0-809 考试。即使此考试不是通过 1Z0-809 考试的先决条件,也强烈建议首先参加此考试。1Z0-808 考试涵盖 Java 的基础知识,而 1Z0-809 考试涵盖高级 Java 概念。

Java 初级认证 (1Z0-811) 是 Oracle 于 2016 年推出的一项新认证。它是一项面向中学、两年制学院和四年制学院及大学学生的入门级认证。所有其他 Java 认证都是职业级认证。如图 1 所示,Java 认证轨迹提供在助理、专业、专家和大师类别下。

4. 比较 OCA Java 考试版本

本节将澄清关于 OCA Java 考试不同版本所存在的任何混淆。截至目前,Oracle 提供了三种版本的 OCA Java 认证:

  • OCA Java SE 8 程序员 I (考试编号:1Z0-808)

  • OCA Java SE 7 程序员 I (考试编号:1Z0-803)

  • OCA Java SE 5/SE 6 (考试编号:1Z0-850)

表 2 比较了这些考试的目标受众、Java 版本、题目数量、考试时长和及格分数。

表 2. 比较考试:OCA Java SE 8 程序员 I、OCA Java SE 7 程序员 I 和 OCA Java SE 5/6
OCA Java SE 8 程序员 I (1Z0-803) OCA Java SE 7 程序员 I (1Z0-803) OCA Java SE 5/SE 6 (1Z0-850)
目标受众 Java 程序员 Java 程序员 Java 程序员和 IT 管理员
Java 版本 8 7 6
总题数 77 70 51
考试时长 150 分钟 120 分钟 115 分钟
及格分数 65% 63% 68%

OCA Java SE 8 程序员 I 认证在 OCA Java SE 7 程序员 I 认证涵盖的主题基础上增加了以下主题:

  • Java 的特性和组件

  • 包装类

  • 三元构造

  • 一些来自新 Java 8 日期和时间 API 的类

  • 创建和使用 lambda 表达式

  • 断言接口

图 2 展示了 OCA Java SE 8 和 OCA Java SE 7 程序员 I 考试目标的详细比较。以下是对图例的解释:

  • 浅灰色背景— 主要考试目标。

  • 中等灰色背景— 仅在 OCA Java SE 8 考试中涵盖。

  • 深灰色背景— 尽管此子目标的文本或主要考试目标不同,但它被其他考试涵盖。

图 2. 比较 OCA Java SE 8 程序员 I 和 OCA Java SE 7 程序员 I 认证的考试目标

图 3 展示了 OCA Java SE 5/6(1Z0-850)和 OCA Java SE 7 程序员 I(1Z0-803)考试目标的详细比较。它显示了这些考试版本独有的目标和两者共有的目标。第一列显示了仅包含在 OCA Java SE 5/6(1Z0-850)中的目标,中间列显示了共同考试目标,右侧列显示了仅在 OCA Java SE 7 程序员 I(1Z0-803)中涵盖的考试目标。

图 3. 比较 OCA Java SE 5/6 和 OCA Java SE 7 程序员 I 的考试目标

5. 下一步:OCP Java SE 8 程序员 II(1Z0-809)考试

在成功通过 OCA Java SE 8 程序员 I 考试后,下一步是参加 OCP Java SE 8 程序员 II 考试。OCP Java SE 8 程序员 II 认证是为具有 Java 编程语言高级技能的个人设计的。它涵盖了高级 Java 特性,如线程、并发、集合、Streams API、Java 文件 I/O、内部类、本地化等。

6. 完成考试目标,映射到书籍章节,并准备清单

表 3 包含了 OCA Java SE 8 程序员 I 考试的完整考试目标列表,这些目标来自 Oracle 的网站。所有目标都映射到涵盖它们的书籍章节和部分编号。

表 3. 考试目标和子目标与章节和部分编号的映射
考试目标 包含在章节/部分
1 Java 基础知识 第一章 和 第三章
1.1 定义变量的作用域 第 3.1 节
1.2 定义 Java 类的结构 第 1.1 节
1.3 使用主方法创建可执行的 Java 应用程序;从命令行运行 Java 程序,包括控制台输出 第 1.2 节
1.4 导入其他 Java 包以使它们在您的代码中可用 第 1.3 节
1.5 比较和对比 Java 的特性组件,例如平台无关性、面向对象、封装等 第 1.6 节
2 使用 Java 数据类型 第二章 和 第三章
2.1 声明和初始化变量(包括原始数据类型的类型转换) 第 2.1 节 和 第 2.3 节
2.2 区分对象引用变量和原始变量 第 2.1 节 和 第 2.3 节
2.3 了解如何读取和写入对象字段 第 3.6 节
2.4 解释一个对象的生命周期(创建、通过重新赋值进行“解引用”和垃圾回收) 第 3.2 节
2.5 开发使用包装类(如 Boolean、Double 和 Integer)的代码 第 2.5 节
3 使用运算符和决策构造 第二章, 第四章 和 第五章
3.1 使用 Java 运算符,包括括号来覆盖运算符优先级 第 2.4 节
3.2 使用 == 和 equals() 测试字符串和其他对象之间的相等性 第 4.1 节 和 第 4.5 节
3.3 创建 if 和 if/else 以及三元构造 第 5.1 节
3.4 使用 switch 语句 第 5.2 节
4 创建和使用数组 第四章
4.1 声明、实例化、初始化和使用一维数组 第 4.3 节
4.2 声明、实例化、初始化和使用多维数组 第 4.3 节
5 使用循环构造 第五章
5.1 创建和使用 while 循环 第 5.5 节
5.2 创建和使用 for 循环,包括增强型 for 循环 第 5.3 节 和 第 5.4 节
5.3 创建和使用 do-while 循环 第 5.5 节
5.4 比较循环构造 第 5.6 节
5.5 使用 break 和 continue 第 5.7 节
6 使用方法和封装 第一章 和 第三章
6.1 创建具有参数和返回值的方法,包括重载方法 第 3.3 节 和 第 3.4 节
6.2 将 static 关键字应用于方法和字段 第 1.5 节
6.3 创建和重载构造函数,包括对默认构造函数的影响 第 3.5 节
6.4 应用访问修饰符 第 1.4 节
6.5 将封装原则应用于一个类 第 3.7 节
6.6 确定当将对象引用和原始值传递给会改变其值的方法时对它们的影响 第 3.8 节
7 处理继承 第一章 和 第六章
7.1 描述继承及其优势 第 6.1 节 和 第 6.2 节
7.2 开发演示多态使用的代码,包括重写和对象类型与引用类型 第 6.3 节 和 第 6.6 节
7.3 确定何时需要进行类型转换 第 6.4 节
7.4 使用 super 和 this 访问对象和构造函数 第 6.5 节
7.5 使用抽象类和接口 第 1.5 节、第 6.1 节、第 6.2 节 和 第 6.6 节
8 处理异常 第七章
8.1 区分检查异常、非检查异常和错误 第 7.2 节
8.2 创建 try-catch 块并确定异常如何改变正常程序流程 第 7.4 节
8.3 描述异常处理的优势 第 7.1 节
8.4 创建并调用抛出异常的方法 第 7.3 节 和 第 7.4 节
8.5 识别常见的异常类(如 NullPointerException、Arithmetic-Exception、ArrayIndexOutOfBoundsException、ClassCastException) 第 7.5 节
9 使用 Java API 中的选定类 第四章 和 第六章
9.1 使用 StringBuilder 类及其方法操作数据 第 4.2 节
9.2 创建和操作字符串 第 4.1 节
9.3 使用 java.time.Local-DateTime、java.time.LocalDate、java.time.LocalTime、java.time.format.DateTimeFormatter 和 java.time.Period 类创建和操作日历数据 第 4.6 节
9.4 声明并使用特定类型的 ArrayList 第 4.4 节
9.5 编写一个简单的 lambda 表达式,该表达式消费一个 lambda 谓词表达式 第 6.7 节

7. 常见问题

当你开始考试准备或甚至考虑获得认证时,你可能会感到焦虑。本节可以通过回答关于考试准备和参加考试的一些常见问题来帮助你平静下来。

7.1. 关于考试准备的常见问题

本节回答了关于如何准备考试的常见问题,包括最佳方法、学习材料、准备时间、考试中的题型等。

OCA Java SE 8 程序员 I 级考试的考试详情是否会改变?

即使认证已经上线,Oracle 也可以更改认证的考试详情。这些更改可能包括考试目标、价格、考试时长、考试问题以及其他部分。在过去,Oracle 已经对认证考试进行了类似的更改。这些更改可能不是重大的,但当你开始准备考试时,始终建议检查 Oracle 的网站以获取最新的考试信息。

准备这个考试的最佳方法是什么?

通常,考生会结合使用多种资源,如书籍、在线学习材料、关于考试的论文、免费和付费的模拟考试以及培训来准备考试。不同的组合对不同的人效果最好,没有一种完美的准备公式。根据培训或自学对你来说效果更好,你可以选择最适合你的方法。结合大量的代码实践和模拟考试。

我如何知道我是否为考试做好了准备?

你可以通过在模拟考试中持续获得好成绩来确保你的考试准备情况。一般来说,在连续进行的约三到五次模拟考试中(越多越好)获得 80%以上的分数将确保你在真实考试中也能获得类似的分数。

在真实考试之前我应该尝试多少次模拟考试?

理想情况下,你应该在尝试真正的考试之前至少尝试五次模拟考试。越多越好!

我有两年的 Java 工作经验。我还需要为这个认证做准备吗?

重要的是要理解,实际使用 Java 获得的知识与通过这个认证考试所需的知识是不同的。Java 认证考试的作者使用多种技巧来测试你的知识。因此,你需要有结构化的准备和策略来在认证考试中取得成功。

准备考试的理想时间是多长?

准备时间主要取决于你对 Java 的经验以及你可以用来准备的时间。平均而言,你需要大约 150 小时的课程学习,持续两到三个月来准备这次考试。再次强调,所需的学习时间取决于个人的学习曲线和背景。

保持你的考试准备一致性很重要。你不能学习一个月,然后间隔一个月或更长时间后重新开始。

这个考试包括不计分的问题吗?

在任何 Oracle 考试中,你写的部分问题可能会被标记为不计分。Oracle 的政策规定,在考试过程中,你不会被告知哪些问题会被计分。你可能惊讶地发现,在 OCA Java SE 8 程序员 I 考试中的 77 个问题中,多达 7 个问题可能不计分。即使你回答了一些问题错误,你仍有机会获得 100%的分数。

Oracle 定期更新其所有认证考试的题库。这些不计分的问题可能用于研究和评估可以添加到考试中的新问题。

我可以用模拟考试开始我的考试准备吗?

如果你非常熟悉 Java 语言特性,那么是的,你可以用模拟考试开始你的考试准备。这也有助于你了解在实际认证考试中可以期待的问题类型。但是,如果你在 Java 方面几乎没有经验,或者如果你对 Java 的语言特性不太熟悉,我不建议你从模拟考试开始。考试作者经常使用很多技巧来评估实际认证考试中的候选人。用模拟考试开始你的考试准备只会让你对 Java 概念感到困惑。

我真的需要去认证吗?

是的,你应该这样做,简单的理由是雇主关心员工的认证。组织更倾向于有认证的 Java 开发者,而不是具有相似 IT 技能和经验的非认证 Java 开发者。认证还可以让你比没有认证的具有相似技能的同行获得更高的薪水。

我需要做任何假设吗?

是的,Oracle 已经在其网站上发布了以下针对候选人的假设(如前所述,Oracle 可能会在没有任何事先通知的情况下更改考试细节或假设):

  • 缺少packageimport语句——如果示例代码没有包含packageimport语句,并且问题没有明确提到这些缺失的语句,那么假设所有示例代码都在同一个包中,并且存在导入语句来支持它们。

  • 没有为类指定文件或目录路径名—— 如果问题没有说明类的文件名或目录位置,那么假设以下之一, whichever will enable the code to compile and run:

    • 所有类都在一个文件中。

    • 每个类都包含在一个单独的文件中,所有文件都在一个目录中。

  • 意外的换行符— 样本代码可能会有意外的换行符。如果你看到一行代码看起来像是被换行了,并且这造成了一个换行有意义的情境(例如,一个引号内的String字面量被换行),假设换行是同一行的扩展,该行不包含会导致编译失败的硬回车符。

  • 代码片段— 代码片段是源代码的一个小部分,它在不包含其上下文的情况下呈现。假设所有必要的支持代码都存在,并且支持环境完全支持所显示代码及其省略环境的正确编译和执行。

  • 描述性注释— 对描述性注释,如“setter 和 getters 在这里”,按字面意思理解。假设正确的代码存在,可以编译并成功运行以创建所描述的效果。

我在考试中可以期待哪些类型或格式的题目?

考试使用不同格式的多项选择题,本节通过八个带有图例的示例题目来说明。

所有这些题型示例展示了以下主题集合可能如何使用不同的题型进行测试:

  • main方法的正确声明

  • 传递命令行参数

  • 重载方法

  • 方法参数名称的重要性

  • 可变参数变量的声明

考试题型 1 (图 4)—包括简单的代码,但答案选项可能复杂或令人困惑。

图 4. 考试题型 1

图片

以下示例中的答案选项可能会让读者困惑,不知道命令行值是连接还是作为整数值相加:

图片

注意

在本书中,每章末尾的样题和书末的全真模拟考试将答案选项以字母(例如,a–d)的形式呈现,以便于讨论。然而,在考试中,答案选项既不编号也不字母化。它们前面是单选按钮或复选框。单选按钮用于只有一个正确答案的问题,而复选框用于有多个正确答案的问题。

考试题型 2 (图 5)—没有代码的考试题目能让你从阅读代码中获得必要的休息。但回答它们并不总是那么容易。

图 5. 考试题型 2

图片

考试题型 2 的一个示例:

图片

考试题型 3 (图 6)—阅读和理解大量代码可能很困难。关键是排除错误答案,快速找到正确答案。

图 6. 考试题型 3

图片

示例:

图片

考试题型 4 (图 7)—这类问题是“填空题”的经典例子。

图 7. 考试题型 4

图片

示例:

图片

考试题型 5 (图 8)—此题型将包括代码、条件或两者兼而有之。答案选项将包括对代码中应用的变化及其结果。除非另有说明,否则您选择的答案选项中的变化将单独应用于代码或指定的情况。正确答案选项的结果不会涉及其他正确答案选项中建议的变化。

图 8. 考试题型 5

示例:

考试题型 6 (图 9)—因为您的思维模式是选择正确选项,所以请非常仔细地回答这类问题。我的个人建议:用一只手交叉手指来提醒自己需要选择错误的陈述。

图 9. 考试题型 6

示例:

考试题型 7 (图 10)—此问题在问题文本中不会包括任何代码;它将声明需要使用答案选项中给出的代码实现的条件。

图 10. 考试题型 7

示例:

考试题型 8 (图 11)—此题型包括单个或多维数组的图示,陈述一个情况并要求您选择代码作为输入以获得所需的数组格式。

图 11. 考试题型 8

示例:

7.2. 关于参加考试的问题解答

本节包含与考试注册、考试券、考试时的注意事项以及重考考试相关的一些常见问题列表。

我在哪里以及如何参加这次考试?

您可以在 Oracle 测试中心或 Pearson VUE 授权测试中心参加这次考试。为了参加考试,您必须注册考试并购买考试券。以下选项可供选择:

  • 直接注册考试并支付 Pearson VUE 费用

  • 从 Oracle 购买考试券并在 Pearson VUE 注册参加考试

  • 在 Oracle 测试中心注册

寻找您所在地区的最近测试中心,注册并安排考试日期和时间。大多数流行的计算机培训机构在其场所也设有测试中心。您可以在www.pearsonvue.com/oracle/找到 Pearson VUE 测试站点,其中包含有关查找测试中心和安排或重新安排考试的具体信息。在注册时,您需要提供以下详细信息,包括您的姓名、地址和联系电话:

  • 考试标题和编号(OCA Java SE 8 程序员 I,1Z0-808)

  • 在注册期间应应用的任何折扣代码

  • Oracle 测试 ID/考生 ID,如果您已经参加了任何其他 Oracle/Sun 认证考试

  • 如果你的雇主在 Oracle 合作伙伴网络中(如果你的雇主在 Oracle 合作伙伴网络中,你可以找到公司 ID 并使用任何可用的考试费折扣)

我需要携带我的照片身份证明或其他证明吗?

考试中心协调员会要求你至少提供两种身份证明,其中之一必须包含你的照片。如果有疑问,请通过电子邮件或电话与你的考试中心联系,并询问身份证明要求。

考试券的有效期是多久?

每张考试券上都印有有效期。警惕任何声称可以在过期日期之后使用的折扣券。

我可以在参加这次考试时参考笔记或书籍吗?

在参加这次考试时,你不能参考任何书籍或笔记。你不被允许携带任何空白纸用于草稿或甚至将手机带入测试隔间。

在考试过程中标记问题的目的是什么?

通过标记问题,你可以有效地管理你的时间。不要在一个问题上花费太多时间。你可以在考试过程中标记一个难题,稍后再回答。考试结束时,你可以选择回顾标记问题的答案。此外,使用“上一题”和“下一题”按钮在问题之间导航通常很耗时。如果你不确定答案,可以标记它并在考试结束时回顾。

我可以写下考试问题并随身携带吗?

不。考试中心不再提供你在考试期间可能需要的草稿纸。测试中心将提供可擦除或不可擦除的板子。如果你被提供的是不可擦除的板子,如果你需要,你可以要求另一块。

Oracle 对认证候选人以任何形式分发或传播记忆中的问题非常挑剔。如果 Oracle 发现这种情况发生,它可能会取消候选人的证书,永远禁止该候选人参加任何 Oracle 认证,通知雇主或采取法律行动。

如果我在总时间之前或之后完成考试会发生什么?

如果你在大约的总考试时间结束前完成了考试,请修改你的答案并点击“提交”或“完成”按钮。如果你没有点击“提交”按钮并且用完了所有的考试时间,考试引擎将不再允许你修改任何考试答案,并将显示带有“提交”按钮的屏幕。

考试结束后我会立即收到我的分数吗?

不,你不会。当你点击“提交”按钮后,大约半小时后屏幕会要求你登录到你的 Oracle 账户(CertView)以查看分数。它还包括你回答错误的话题。测试中心不会给你任何关于你的认证分数的纸质副本。证书本身将在六到八周内通过邮寄方式送达。

如果我考试不及格,我可以重新参加考试吗?

这并不是世界末日。如果您失败了,请不要担心。您可以在 14 天后(而且世界不会知道这是重考)重新参加考试。

但您不能通过重考已通过考试来提高分数。同样,您也不能重考 Beta 考试。

8. 考试中使用的测试引擎

用于认证考试的测试引擎的用户界面相当简单。(与今天的网络、桌面和智能手机应用程序相比,甚至可以说是原始的。)

在您开始考试之前,您将需要接受 Oracle 认证候选人协议的条款和条件。您的计算机屏幕将显示所有这些条件,并给您一个接受这些条件的选择。只有接受这些条件,您才能开始答题。

这里是 Oracle 使用的测试引擎的功能:

  • 引擎用户界面分为三个部分—测试引擎的用户界面分为以下三个部分:

    • 静态上部部分—显示问题编号、剩余时间和一个复选框以标记问题为复习

    • 可滚动中间部分—显示问题文本和答案选项

    • 静态底部部分—显示显示上一个问题、显示下一个问题、结束考试和查看标记问题的按钮

  • 每个问题都显示在单独的屏幕上—考试引擎一次在屏幕上显示一个问题。它不会像可滚动的网页那样在单个屏幕上显示多个问题。我们尽力在不滚动或仅滚动很少的情况下显示完整的问题和答案选项。

  • 代码展示按钮—许多问题包含代码。这些问题及其答案可能需要大量滚动才能查看。因为这可能相当不方便,所以这些问题包括一个代码展示按钮,该按钮在单独的窗口中显示代码。

  • 标记要复习的问题—问题屏幕在左上角显示一个带有“标记为复习”文本的复选框。可以使用此选项标记问题。在考试结束时,可以快速复习标记的问题。

  • 显示上一个和下一个问题的按钮— 测试包括在测试引擎底部显示上一个和下一个问题的按钮。

  • 结束考试和查看标记问题的按钮— 引擎在测试引擎的底部显示结束考试和查看标记问题的按钮。

  • 剩余时间— 引擎在屏幕右上角显示考试的剩余时间。

  • 问题编号— 每个问题显示其序列号。

  • 正确答案选项的数量— 每个问题显示应从多个选项中选择正确数量的选项。

我代表所有在 Manning 出版公司工作的人,祝您考试顺利,并希望您在考试中取得很好的成绩。

第一章 Java 基础知识

本章涵盖的考试目标 你需要了解的内容
[1.2] 定义 Java 类的结构。 Java 类的结构,包括其组件:包和导入语句、类声明、注释、变量和方法。Java 类组件与 Java 源代码文件组件之间的区别。
[1.3] 使用具有 main 方法的 Java 应用程序创建可执行文件;从命令行运行 Java 程序;包括控制台输出。 创建可执行 Java 应用程序的 main 方法的正确方法签名。传递给 main 方法的参数。
[1.4] 将其他 Java 包导入到你的代码中以便使用。 理解包和导入语句。获取从包和接口导入类到自己的类中的正确语法和语义。
[6.4] 应用访问修饰符。 将访问修饰符(public、protected、默认和 private)应用于类及其成员。确定这些修饰符的代码可访问性。
[7.5] 使用抽象类和接口。 定义类、接口和方法作为抽象实体的含义。
[6.2] 将静态关键字应用于方法和字段。 将字段和方法定义为静态成员的含义。
[1.5] 比较和对比 Java 的特性如:平台无关性、面向对象、封装等。 与 Java 相关的特性和组件。

想象你正在建立一个与多个开发者合作的 IT 组织。为了确保工作顺利高效,你将为你的组织定义一个结构以及一组具有单独职责的部门。这些部门在需要时将相互协作。此外,根据保密性要求,你的组织的数据将根据需要提供给员工,或者你可以将特殊权限仅分配给组织的某些员工。这是组织如何通过一个明确的结构和一套规则来提供最佳结果的例子。

同样,Java 有一个明确的结构和层次。组织的结构和组件可以与 Java 的类结构和组件进行比较,组织的部门可以与 Java 包进行比较。限制组织中对某些数据的访问可以与 Java 的访问修饰符进行比较。组织的特殊权限可以与 Java 中的非访问修饰符进行比较。

在 OCA Java SE 8 程序员 I 考试中,你将被问到 Java 类的结构、包、导入类以及应用访问和非访问修饰符和 Java 的特性和组件。鉴于这些信息,本章将涵盖以下内容:

  • Java 类的结构和组件

  • 理解可执行的 Java 应用程序

  • 理解 Java 包

  • 将 Java 包导入到你的代码中

  • 应用访问和非访问修饰符

  • Java 的特性和组件

1.1. Java 类和源代码文件的结构

[1.2] 定义 Java 类的结构

注意

当你看到像前面那样的认证目标时,这意味着在本节中我们将涵盖这个目标。同一个目标可能在本章或其他章节的多个部分中都有涉及。

本节涵盖了 Java 源代码文件 (.java 文件) 和 Java 类(使用关键字 class 定义)的结构和组件。它还涵盖了 Java 源代码文件和 Java 类之间的区别。

首先,先从对认证考试中对你有什么要求有一个清晰的理解开始准备考试。例如,尝试回答一个认证申请者的以下问题:“我遇到了‘类’这个术语的不同含义:类 Person、Java 源代码文件(Person.java)和存储在 Person.class 中的 Java 字节码。这些结构中哪一个在考试中?”要回答这个问题,请查看图 1.1,它包括了类 Person、文件 Person.java 和 Person.class 以及它们之间的关系。

图 1.1. 类文件 Person 与文件 Person.java 和 Person.class 之间的关系以及它们如何相互转换

如图 1.1 所示,一个人可以被定义为一个类 Person。这个类应该位于一个 Java 源代码文件(Person.java)中。使用这个 Java 源代码文件,Java 编译器(Windows 上的 javac.exe 或 Mac OS X/Linux/UNIX 上的 javac)生成字节码(Java 虚拟机的编译代码)并将其存储在 Person.class 中。本考试目标的范围仅限于 Java 类(类 Person)和 Java 源代码文件(Person.java)。

1.1.1. Java 类的结构

OCA Java SE 8 程序员 I 考试将就 Java 源文件的结构和组件以及在其中可以定义的类或接口向你提问。图 1.2 展示了 Java 类文件的组件(接口将在第六章中详细讲解)。

图 1.2. Java 类的组件

在本节中,我将讨论所有 Java 类文件组件。让我们从 package 语句开始。

注意

本书中的代码不包含很多空格——它模仿了你在考试中会看到的代码类型。但当你处理实际项目时,我强烈建议你使用空格或注释来使你的代码可读。

包声明

所有的 Java 类都是包的一部分。Java 类可以显式地定义在命名包中;否则,它将成为一个默认包的一部分,该包没有名称。

package 语句用于显式地定义类所在的包。如果一个类包含 package 语句,它必须是类定义中的第一个语句:

注意

包的详细内容在本章的 1.3 节 中介绍。

package 语句不能出现在类声明中或类声明之后。以下代码将无法编译:

以下代码将无法编译,因为它在类定义中将 package 语句放在了内部:

此外,如果存在,package 语句必须恰好出现在类中一次。以下代码将无法编译:

导入语句

同一包中的类和接口可以相互使用,而无需在它们的名称前加上包名。但为了使用来自另一个包的类或接口,你必须使用它的完全限定名,即 packageName.anySubpackageName.ClassName。例如,类 String 的完全限定名是 java.lang.String。因为使用完全限定名可能会很繁琐,并且会使你的代码难以阅读,所以你可以使用 import 语句来在你的代码中使用类或接口的简单名称。

让我们通过一个示例类 AnnualExam 来看看这个,该类定义在 university 包中。类 AnnualExam 与类 certification.ExamQuestion 相关联,如图 1.3 所示的统一建模语言(UML)类图所示。

图 1.3. 类 AnnualExamExamQuestion 之间关系的 UML 表示

注意

UML 类图表示应用程序的静态视图。它展示了实体,如包、类、接口及其属性(字段和方法),并描绘了它们之间的关系。它显示了哪些类和接口在包中定义。它描述了类和接口之间的继承关系。它还可以描述它们之间的关联——当一个类或接口定义了另一种类型的属性时。本章中所有的 UML 表示都是类图。考试不涵盖 UML 图。但使用这些快速简单的图可以简化 Java 实体之间的关系——无论是在考试中还是在你的实际项目中。

注意

在整本书中,粗体字体将用于指示我们正在讨论的代码的特定部分,或代码中的更改或修改。

这是类AnnualExam的代码:

注意,import语句位于package语句之后,但在class声明之前。如果类AnnualExam未在包中定义,会发生什么?如果AnnualExamExamQuestion类如图 1.4 所示相关联,代码会有任何变化吗?

图 1.4. 无包的类AnnualExamExamQuestion之间的关系

在这种情况下,类AnnualExam不是显式包的一部分,但类ExamQuestioncertification包的一部分。以下是类AnnualExam的代码:

如前一个示例代码所示,类AnnualExam没有定义package语句,但它定义了import语句来导入certification.ExamQuestion类。

如果类中存在package语句,则import语句必须跟在package语句之后。保持packageimport语句出现的顺序很重要。颠倒这个顺序会导致你的代码无法编译:

我们将在本章的 1.3 节中详细讨论import语句。

注释

你也可以在 Java 代码中添加注释。注释可以出现在类中的多个位置。注释可以出现在package语句之前和之后,类定义之前和之后,以及方法定义之前和之后。注释有两种类型:多行注释和行尾注释。

多行注释跨越多行代码。它们以/*开始,以*/结束。以下是一个示例:

多行注释可以包含特殊字符。以下是一个示例:

在前面的代码中,注释不是每行都以星号开头。但大多数时候,当你看到 Java 源代码文件(.java 文件)中的多行注释时,你会注意到它使用星号(*)在下一行开始注释。请注意,这不是必需的——这样做更多的是为了美观。以下是一个示例:

行尾注释以//开头,正如其名称所示,它们放置在代码行末或空白行上。//和行尾之间的文本被视为注释,你通常使用它来简要描述代码行。以下是一个示例:

虽然在以下代码中使用多行注释不常见,但考试要求你了解代码是有效的:

当你在赋字符串值时在引号内包含多行注释,会发生以下情况:

当包含在双引号内时,多行注释被视为普通字符,而不是注释。因此,以下代码无法编译,因为分配给变量 name 的值是一个未闭合的字符串字面值:

在前面的 package 语句部分,您了解到如果存在,则 package 语句应该是类中代码的第一行。这个规则的唯一例外是注释的存在。注释可以出现在 package 语句之前。以下代码定义了一个 package 语句,其中包含多行和行尾注释:

定义了多行代码中的行尾代码注释。这是可接受的。行尾代码注释被视为多行注释的一部分,而不是单独的行尾注释。行 定义了行尾代码注释。行 定义了行首的行尾代码注释,在类定义之后。

多行注释放置在 package 语句之前,这是可接受的,因为注释可以出现在代码的任何位置。

Javadoc 注释

Javadoc 注释是 Java 源文件中以 /** 开始并以 */ 结束的特殊注释。这些注释由 Javadoc 工具处理,以生成您的 Java 源代码文件的 API 文档。要查看其工作情况,请比较类 String 的 API 文档及其源代码文件(String.java)。

类声明

类声明标志着类的开始。它可以像关键字 class 后跟类名这样简单:

类的声明由以下部分组成:

  • 访问修饰符

  • 非访问修饰符

  • 类名

  • 基类名称,如果该类扩展了另一个类

  • 如果类实现了任何接口,则所有实现的接口

  • 类体(类字段、方法、构造函数),包含在一对大括号 {}

如果您现在不理解这部分内容,请不要担心。我们将在准备考试的过程中详细讲解这些内容。

让我们通过一个例子来看看类声明的组成部分:

public final class Runner extends Person implements Athlete {}

前一个类声明的组成部分可以如图 1.5 所示进行说明。

图 1.5. 类声明的组成部分

表 1.1 总结了必需和可选组件。

表 1.1. 类声明的组成部分
必需 可选
关键字 class 访问修饰符,例如 public
类名 非访问修饰符,例如 final
类体,由大括号 {} 标记 关键字 extends 以及基类的名称
关键字 implements 以及实现接口的名称

我们将在本章的 1.4 节 和 1.5 节 中详细讨论访问和非访问修饰符。

类定义

是用于指定对象属性和行为的设计。对象的属性通过 变量 实现,行为通过 方法 实现。例如,可以将类视为移动电话的设计或规范,而移动电话是该设计的对象。可以使用相同的设计创建多个移动电话,就像 Java 虚拟机 (JVM) 使用类来创建其对象一样。你还可以将类视为一个模具,你可以使用它来创建有意义的和有用的对象。类是一个可以从中创建对象的规范。

让我们定义一个简单的类来表示移动电话:

class Phone {
    String model;
    String company;
    Phone(String model) {
        this.model = model;
    }
    double weight;
    void makeCall(String number) {
        // code
    }
    void receiveCall() {
        // code
    }
}

要点:

  • 类名以关键字 class 开头。注意关键字 class 的大小写。Java 是大小写敏感的。class(小写 c)与 Class(大写 C)不同。你不能使用单词 Class(大写 C)来定义一个类。

  • 类的状态是通过属性或实例变量定义的。

  • 在定义类的方法之前,不一定必须定义所有属性(变量 weightPhone 构造函数之后定义)。但这对于可读性来说远非最佳。

  • 行为是通过方法定义的,这些方法可能包括方法参数。

  • 类定义还可以包括注释和构造函数。

注意

类是一个可以从中创建对象的规范。

变量

在前一个示例中重新审视类 Phone 的定义。因为变量 modelcompanyweight 用于存储对象的状态(也称为 实例),它们被称为 实例变量实例属性。每个对象都有自己的实例变量副本。如果你更改一个对象的实例变量的值,则同一名称的实例变量对另一个对象的值不会改变。实例变量是在类中定义的,但位于类中所有方法之外。

一个 类变量static 变量的副本被类中的所有对象共享。static 变量在 1.5.3 节中有详细讨论,包括非访问修饰符 static

方法

再次回顾前面的示例。makeCallreceiveCall 方法是实例方法,通常用于操作实例变量。

如前所述,类方法静态方法 可以用来操作 static 变量。

构造函数

在前面的例子中,Phone类定义了一个单构造函数。类构造函数用于创建和初始化类的对象。一个类可以定义多个构造函数,这些构造函数接受不同的方法参数集。

1.1.2. Java 源代码文件的结构和组件

Java 源代码文件用于定义 Java 实体,例如类、接口、枚举和注解。

注意

Java 注解不在考试范围内,因此本书不会讨论。

所有你的 Java 代码都应该定义在 Java 源代码文件中(以.java 结尾的文本文件)。考试涵盖了 Java 源代码文件结构的以下方面:

  • 在 Java 源代码文件中定义类和接口

  • 在同一 Java 源代码文件中定义单类或多接口

  • importpackage语句应用于 Java 源代码文件中的所有类

我们已经涵盖了第 1.1.1 节中类的详细结构和定义。让我们开始定义接口。

在 Java 源代码文件中定义接口

接口指定了类需要实现的合约。你可以将实现接口比作签订合同。接口是一组相关方法和常量的组合。在 Java 8 之前,接口方法默认是抽象的。但从 Java 8 版本开始,接口中的方法可以定义默认实现。使用 Java 8,接口也可以定义static方法。

这里有一个快速例子,帮助你理解接口的本质。无论我们每个人拥有哪个品牌的电视,每台电视都提供了更改频道和调整音量的通用功能。你可以将电视机的控制面板比作接口,将电视机的设计比作实现接口控制的面板类。

让我们定义这个接口:

interface Controls {
    void changeChannel(int channelNumber);
    void increaseVolume();
    void decreaseVolume();
}

接口的定义以关键字interface开始。记住,Java 是区分大小写的,所以你不能使用单词Interface(带大写的I)来定义接口。本节提供了接口的简要概述。你将在第六章中详细了解接口。

在单个 Java 源代码文件中定义单类和多类

你可以在 Java 源代码文件中定义一个类或一个接口,或者定义多个这样的实体。让我们从一个简单的例子开始:一个名为 Single-Class.java 的 Java 源代码文件,它定义了一个单独的类SingleClass

图片

这里是一个 Java 源代码文件的例子,Multiple1.java,它定义了多个接口:

图片

你也可以在同一个 Java 源代码文件中定义类和接口的组合。以下是一个例子:

图片

在单个 Java 源代码文件中定义多个类或接口不需要特定的顺序。

考试技巧

类和接口可以在 Java 源代码文件中以任何顺序定义。

当你在 Java 源文件中定义一个public类或接口时,类或接口的名称必须与 Java 源文件名称匹配。此外,源代码文件不能定义超过一个public类或接口。如果你尝试这样做,你的代码将无法编译,这会为你提供一个我称为故事中的转折的小型实践练习,如前言中所述。所有这些练习的答案都提供在附录中。

关于故事中的转折练习

对于这些练习,我尝试使用本章中已覆盖的示例中的修改后的代码。故事中的转折标题指的是修改或调整后的代码。

这些练习将帮助你理解即使是微小的代码修改也能改变代码的行为。它们还应该鼓励你仔细检查考试中的所有代码。这些练习的原因是,在考试中,你可能会被问及多个看似需要相同答案的问题。但仔细检查后,你会发现这些问题略有不同,这将改变代码的行为和正确答案选项!

故事中的转折 1.1

修改 Java 源代码文件Multiple.java的内容,并在其中定义一个公共接口。执行代码并查看这种修改如何影响你的代码。

问题:检查以下 Java 源代码文件Multiple.java的内容,并选择正确的选项:

// Contents of Multiple.java
public interface Printable {
    //.. we are not detailing this part
}
interface Movable {
    //.. we are not detailing this part
}

选项:

  1. 一个 Java 源代码文件不能定义多个接口。

  2. 一个 Java 源代码文件只能定义多个类。

  3. 一个 Java 源代码文件可以定义多个接口和类。

  4. 前面的类将无法编译。

如果你需要帮助设置系统以编写 Java,请参考 Oracle 的“入门”教程,docs.oracle.com/javase/tutorial/getStarted/

故事中的转折 1.2

问题:检查以下 Java 源代码文件Multiple2.java的内容,并选择正确的选项(s):

// contents of Multiple2.java
interface Printable {
    //.. we are not detailing this part
}
class MyClass {
    //.. we are not detailing this part
}
interface Movable {
    //.. we are not detailing this part
}
public class Car {
    //.. we are not detailing this part
}
public interface Multiple2 {}

选项:

  1. 代码无法编译。

  2. 代码编译成功。

  3. 移除Car类的定义将使代码编译成功。

  4. Car类改为非公共类将使代码编译成功。

  5. 将接口Multiple2改为非公共接口将使代码编译成功。

Java 源代码文件中包和导入语句的应用

在前面的章节中,我提到你可以在同一个 Java 源代码文件中定义多个类和接口。当你在这类 Java 文件中使用packageimport语句时,这两个语句都适用于该源代码文件中定义的所有类和接口。

例如,如果你在 Java 源代码文件 Multiple.java 中包含一个package和一个import语句(如下面的代码所示),CarMovablePrintable将变成同一包com.manning.code的一部分:

考试提示

在同一 Java 源代码文件中定义的类和接口不能定义在单独的包中。使用import语句导入的类和接口对同一 Java 源代码文件中定义的所有类和接口都是可用的。

在下一节中,你将创建可执行 Java 应用程序——用于定义 Java 应用程序执行入口点的类。

1.2. 可执行 Java 应用程序

[1.3] 使用具有main方法的可执行 Java 应用程序;从命令行运行 Java 程序;包括控制台输出。

OCA Java SE 8 程序员 I 级考试要求你理解可执行 Java 应用程序及其要求的意义,即什么使一个普通 Java 类成为可执行 Java 类。你还需要知道如何从命令行执行 Java 程序。

1.2.1. 可执行 Java 类与非可执行 Java 类

Java 虚拟机在 Java 类被使用时执行所有 Java 类吗?如果是这样,什么是非可执行 Java 类?

当一个可执行 Java 类交给 JVM 时,它将在类中的特定点开始执行——即main方法。JVM 将执行在main方法中定义的代码。你不能将一个非可执行 Java 类(没有main方法的类)交给 JVM 并要求它执行。在这种情况下,JVM 将不知道执行哪个方法,因为没有标记入口点。

通常,一个应用程序由多个类和接口组成,这些类和接口定义在多个 Java 源代码文件中。在这些文件中,程序员会将其中一个类指定为可执行类。程序员可以定义 JVM 启动应用程序时应执行的步骤。例如,程序员可以定义一个包含显示适当 GUI 窗口给用户和打开数据库连接代码的可执行 Java 类。

在图 1.6 中,类WindowUserDataServerConnectionUserPreferences没有定义main方法。类LaunchApplication定义了一个main方法,是一个可执行类。

图 1.6. 类LaunchApplication是一个可执行 Java 类,但其余的类——Window、UserData、ServerConnectionUserPreferences则不是。

注意

一个 Java 应用程序可以定义多个可执行类。当 JVM 开始执行时,我们会选择其中一个(并且恰好一个)。

1.2.2. main方法

创建可执行 Java 应用程序的第一个要求是创建一个方法签名(名称和方法参数)与以下定义的main方法相匹配的类:

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

main方法应遵守以下规则:

  • 该方法必须被标记为public方法。

  • 该方法必须被标记为static方法。

  • 方法名称必须是main

  • 该方法的返回类型必须是void

  • 该方法必须接受一个String数组或String类型的可变参数(varargs)作为方法参数。

图 1.7 展示了前面的代码及其相关规则集。

图 1.7. 正确main方法的组成部分

图片

将传递给main方法的参数定义为String类型的可变参数(varargs)是有效的:

图片

要定义一个可变参数变量,省略号(...)必须跟在变量类型后面,而不是变量本身(许多新程序员都会犯的错误):

图片

如前所述,传递给main方法的String数组名称不必是args才能使其成为正确的main方法。以下也是main方法的正确定义示例:

图片

定义数组时,方括号[]可以跟在变量名或其类型后面。以下也是main方法的正确方法声明:

图片

有趣的是,关键字publicstatic的位置可以互换,这意味着以下都是main方法的正确方法声明:

图片

注意

虽然关键字public staticstatic public都是声明main方法的合法顺序,但public static更常见,因此更易读。

在执行时,如图 1.7 所示的代码将输出以下内容:

Hello exam

如果一个类定义了一个与main方法签名不匹配的main方法,则称为重载方法(重载方法将在第三章中详细讨论)。重载方法是具有相同名称但签名不同的方法。例如,HelloExam类可以定义多个名为main的方法:

图片

在执行时,JVM 将执行main方法,结果输出Hello exam

1.2.3. 从命令行运行 Java 程序

几乎所有 Java 开发者都使用集成开发环境(IDE)。然而,这个考试要求你理解如何使用命令提示符执行 Java 应用程序或可执行的 Java 类。因此,我建议你使用简单的文本编辑器和命令行(即使这可能是你永远不会在现实世界中使用的做法)。

注意

如果您需要帮助设置系统以使用命令提示符编译或执行 Java 应用程序,请参阅 Oracle 的详细说明docs.oracle.com/javase/tutorial/getStarted/cupojava/index.html

让我们回顾一下图 1.7 中显示的代码:

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

要使用命令提示符执行前面的代码,请输入命令java HelloExam,如图 1.8 所示。

图 1.8. 使用命令提示符执行 Java 应用程序

我提到main方法接受一个String数组作为方法参数。但您如何以及在哪里将数组传递给main方法呢?让我们修改之前的代码来访问和输出这个数组中的值:

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

现在,让我们使用命令提示符执行前面的代码,如图 1.9 所示。

图 1.9. 将命令参数传递给main方法

如您从图 1.9 所示的输出中可以看到,关键字java和类的名称并没有作为命令参数传递给main方法。OCA Java SE 8 程序员 I 级考试将测试您对关键字java和类名是否传递给main方法的知识。

考试技巧

传递给main方法的方法参数也称为命令行参数或命令行值。正如其名所示,这些值是从命令行传递给方法的。

如果您在处理数组或String类时没有跟上代码,请不要担心;我们将在第四章中详细讲解String类和数组。

这里是下一个“故事转折”练习。在这个练习中,以及本书的其余部分,您将看到 Shreya、Harry、Paul 和 Selvan 这些名字,他们是假设的程序员,也在为这个认证考试学习。答案通常在附录中提供。

故事转折 1.3

其中一位程序员 Harry 执行了一个程序,输出了java one。现在他正在试图找出以下哪个类输出了这些结果。鉴于他是使用命令java EJava java one one执行这个类的,你能帮助他找出正确的选项(们)吗?

  1. class EJava {
        public static void main(String sun[]) {
            System.out.println(sun[0] + " " + sun[2]);
        }
    }
    
  2. class EJava {
        static public void main(String phone[]) {
            System.out.println(phone[0] + " " + phone[1]);
        }
    }
    
  3. class EJava {
        static public void main(String[] arguments[]) {
            System.out.println(arguments[0] + " " + arguments[1]);
        }
    }
    
  4. class EJava {
        static void public main(String args[]) {
            System.out.println(args[0] + " " + args[1]);
        }
    }
    

与命令行参数的混淆

如果您使用过像 C 这样的语言编程,您可能会对命令行参数感到困惑。像 C 这样的编程语言会将程序的名称作为命令行参数传递给main方法。但 Java 不会将的名称作为参数传递给main方法。

1.3. Java 包

[1.4] 将其他 Java 包导入到您的代码中以便使用

这场考试涵盖了将包导入其他类。但凭借超过十五年的经验,我了解到在开始将其他包导入自己的代码之前,了解包是什么,包中定义的类与未定义在包中的类的区别,以及为什么需要在代码中导入包,这些都非常重要。

在本节中,您将学习 Java 包是什么以及如何创建它们。您将使用import语句,它允许您为在单独的包中定义的类和接口使用简单名称。

1.3.1. 需要包的原因

您认为为什么我们需要包?首先,回答这个问题:您是否记得在您的生命中认识过不止一个 Amit、Paul、Anu 或 John?Harry 认识不止一个 Paul(确切地说,是六个),他将他们分类为经理、朋友和堂兄弟。这些根据他们的位置和关系进行了子分类,如图 1.10 所示。图 1.10。

图 1.10. Harry 认识六个 Paul!

类似地,您可以使用包将一组相关的类和接口组合在一起(这里不会讨论枚举,因为它们在本考试中未涉及)。包还提供访问保护和命名空间管理。您可以为不同的项目创建单独的包来定义类,例如 Android 游戏和在线医疗保健系统。此外,您可以在这些包内创建子包,例如用于 GUI、数据库访问、网络等的不同子包。

注意

在实际项目中,您很少会与没有包的类或接口打交道。几乎所有开发软件的组织都有严格的包命名规则,这些规则通常会被记录下来。

所有类和接口都是在包中定义的。如果您在类或接口中没有包含显式的package语句,它就是默认包的一部分。

1.3.2. 使用包语句在包中定义类

您可以通过将package语句作为代码中的第一个语句来表示一个类或接口是在包中定义的。以下是一个示例:

上述代码中的类在certification包中定义了一个ExamQuestion类。您也可以以类似的方式定义一个接口,例如MultipleChoice

package certification;
interface MultipleChoice {

    void choice1();
    void choice2();
}

图 1.11 显示了一个 UML 类图,描述了包certification与类ExamQuestion和接口MultipleChoice之间的关系。

图 1.11. 显示包certification、类ExamQuestion和接口MultipleChoice之间关系的 UML 类图

在前面的示例中,包的名称是certification。您可以使用这样的名称为只包含几个类和接口的小项目命名,但组织通常使用子包来定义所有它们的类。例如,如果 Oracle 的人要定义一个用于存储 Java Associate 考试问题的类,他们可能会使用包名com.oracle.javacert.associate。图 1.12 显示了其 UML 表示,以及相应的类定义。

图 1.12. 子包及其相应的类定义

包由多个部分组成,从更通用的(左侧)到更具体的(右侧)。包名 com.oracle.javacert.associate 遵循 Oracle 推荐的包命名约定,并在 表 1.2 中展示。

表 1.2. 在包名 com.oracle.javacert.associate 中使用的包命名约定
包或子包名称 其含义

| com | 商业。常用的三个字母包缩写之一是

  • gov—for government bodies

  • edu—for educational institutions

|

oracle 组织名称
javacert Oracle 项目进一步分类
associate Java 认证进一步细分
需要记住的规则

关于包的一些重要规则如下:

  • 根据 Java 命名约定,包名应全部小写。

  • 包和子包名称使用点 (.) 分隔。

  • 包名遵循 Java 中有效标识符定义的规则。

  • 对于在包中定义的类和接口,package 语句是 Java 源文件(.java 文件)中的第一个语句。例外情况是注释可以出现在 package 语句之前或之后。

  • 每个 Java 源代码文件 (.java 文件) 中最多只能有一个 package 语句。

  • 在 Java 源代码文件中定义的所有类和接口都在同一个包中定义。它们不能在单独的包中定义。

注意

类或接口的完全限定名是通过在其包名前加上其名称(用点分隔)来形成的。在图 1.11 中,ExamQuestion 类的完全限定名是 certification.ExamQuestion,在图 1.12 中是 com.oracle.javacert.associate.ExamQuestion

目录结构和包层次结构

包中定义的类和接口的层次结构必须与代码中定义这些类和接口的目录层次结构相匹配。例如,certification 包中的 ExamQuestion 类应在名为certification的目录中定义。目录certification的名称和位置由图 1.13 中所示的规则所控制。

图 1.13. 匹配的目录结构和包层次结构

对于图 1.13 中所示的包示例,请注意,定义目录结构的基目录位置没有限制,如图 1.14 所示。

图 1.14. 定义目录结构对应包层次结构的基本目录位置没有限制。

设置打包类的类路径

要使 Java 运行时环境 (JRE) 能够找到您的类,请将包含您的打包 Java 代码的基本目录添加到类路径中。

例如,为了使 JRE 能够定位到前面例子中的certification.ExamQuestion类,将 C:\MyCode 目录添加到 classpath 中。为了使 JRE 能够定位到类com.oracle.javacert.associate.ExamQuestion,将 C:\ProjectCode 目录添加到 classpath 中。

注意

如果你使用的是 IDE,你不需要麻烦设置 classpath。但我强烈建议你学习如何使用简单的文本编辑器以及如何设置 classpath。这可以在你的工作中有所帮助。考试期望你能够发现带有编译错误的代码,如果你没有学习如何在没有 IDE 的情况下做到这一点(IDE 通常包括代码自动纠正或自动完成功能),这并不容易。

1.3.3. 使用 import 语句的简单名称

import语句允许你使用简单名称而不是使用完全限定名称来引用定义在单独包中的类和接口。

让我们用一个现实生活中的例子来工作。想象一下你的家和你的办公室。家里的客厅和厨房可以相互引用,而不必提到它们存在于同一个家中。同样,在办公室里,隔间和会议室可以相互引用,而不必明确提到它们存在于同一个办公室。但是,如果没有说明它们存在于不同的家或办公室,Home 和 Office 就不能访问对方的房间或隔间。这种情况在图 1.15 中得到了表示。

图 1.15. 为了相互引用成员,Home 和 Office 应该指定它们存在于不同的地方。

图片

要引用隔间中的客厅,你必须指定其完整位置,如图图 1.16 的左侧所示。正如你可以从这张图中看到的,对客厅位置的重复引用使得对客厅的描述看起来繁琐且重复。为了避免这种情况,你可以在隔间中显示一个通知,说明所有对客厅的引用都指的是家中的客厅,之后可以使用其简单名称。Home 和 Office 就像 Java 包一样,这个通知相当于import语句。图 1.16 展示了在隔间中使用完全限定名称和简单名称对客厅的差别。

图 1.16. 在隔间中,可以通过使用其完全限定名称来访问客厅。如果你也使用import语句,也可以使用其简单名称来访问。

图片

让我们在代码中实现前面的例子,其中LivingRoomKitchen类定义在home包中,而CubicleConferenceHall类定义在office包中。类Cubicle使用(关联到)home包中的LivingRoom类,如图图 1.17 所示。

图 1.17. 定义在单独包中的LivingRoomCubicle类的 UML 表示,以及它们之间的关联

图片

Cubicle 可以不使用 import 语句来引用类 LivingRoom

图片

Cubicle 可以通过使用 import 语句来使用类 LivingRoom 的简单名称:

图片

注意

import 语句不会将导入类的内容嵌入到你的类中,这意味着导入更多类不会增加你自己的类的大小。

1.3.4. 不使用 import 语句使用包类

可以使用完全限定的名称,而不使用 import 语句来使用包中的类或接口:

图片

但如果你创建了多个在其他包中定义的接口和类的变量,使用完全限定的类名可能会使你的代码变得杂乱。不要在实际项目中使用这种方法。

考试技巧

使用 java.lang 包的成员不需要显式的 import 语句。此包中的类和接口在 所有 其他 Java 类、接口或枚举中自动导入。

对于考试,重要的是要注意你不能使用 import 语句从不同包中访问具有相同名称的多个类或接口。例如,Java API 在两个常用包中定义了 Date 类:java.utiljava.sql。要在类中定义这些类的变量,请使用变量声明中的完全限定名称:

图片

尝试在同一个类中使用 import 语句导入这两个类将无法编译:

图片

另一种方法(在实际项目中效果很好)是使用你使用频率较高的类或接口的 import 定义,并完全引用你偶尔使用的那个:

图片

1.3.5. 导入单个成员与导入包的所有成员

你可以使用 import 语句导入包的单个成员或所有成员(类和接口)。首先,回顾一下 certification 包的 UML 表示,如图 1.18 所示。

图 1.18. certification 包的 UML 表示

图片

检查以下 AnnualExam 类的代码:

图片

通过使用通配符,星号 (*),你可以导入一个包的所有 public 成员、类和接口。比较以下类定义与 AnnualExam 类的以下定义:

图片

注意

当过度使用时,使用星号导入一个包的所有成员会有一个缺点。可能更难确定哪个导入的类或接口来自哪个包。

当你使用 IDE 时,它可能会自动添加你在代码中引用的类和接口的 import 语句。

1.3.6. import 语句不会导入整个包树

你不能使用import语句中的星号导入子包中的类。例如,图 1.19 中的 UML 表示法描述了包含Schedule类和两个子包associatewebdeveloper的包com.oracle.javacert。包associate包含ExamQuestion类,而包webdeveloper包含MarkSheet类。

图 1.19. 包com.oracle.javacert及其子包的 UML 表示

以下import语句只会导入Schedule类。它不会导入ExamQuestionMarkSheet类:

同样,以下import语句会导入associatewebdeveloper包中的所有类:

1.3.7. 从默认包导入类

如果你在类或接口中不包括包声明,会发生什么?在这种情况下,它们将成为一个默认、无名称包的一部分。这个默认包会自动导入你系统同一目录中定义的 Java 类和接口。

例如,未在显式包中定义的PersonOffice类,如果它们定义在同一个目录中,则可以相互使用:

来自默认包的类无法在任何命名包的类中使用,无论它们是否定义在同一个目录中。

考试技巧

命名包的成员无法访问在默认包中定义的类和接口。

1.3.8. 静态导入

你可以使用import static语句导入一个类的单个static成员或所有static成员。虽然可以通过实例访问,但最好通过在名称前加上类或接口名称来访问static成员。通过使用static import,你可以省略前缀,只需使用static变量或方法的名称。在以下代码中,类ExamQuestion定义了一个public static变量marks和一个public static方法print

使用import static语句,可以在AnnualExam类中访问marks变量。importstatic关键字的顺序不能颠倒:

考试技巧

这个特性被称为静态导入,但其语法是import static

要在AnnualExam类中访问ExamQuestion类的所有publicstatic成员,而不需要单独导入每个成员,你可以使用import static语句中的星号:

由于marks变量和方法print被定义为public成员,它们对AnnualExam类是可访问的。通过使用import static语句,你不需要在它们前面加上它们的类名。

注意

在实际项目中,避免过度使用静态导入;否则,代码可能会变得有些混乱,不清楚哪个导入组件来自哪个类。

类、接口及其方法和变量的可访问性由它们的访问修饰符决定,这些将在下一节中介绍。

1.4. Java 访问修饰符

[6.4] 应用访问修饰符

在本节中,我们将涵盖所有访问修饰符——publicprotectedprivate——以及默认访问,这是当你不使用访问修饰符时的结果。我们还将探讨如何使用访问修饰符来限制同一包和不同包中类及其成员的可访问性。

1.4.1. 访问修饰符

让我们从一个例子开始。检查以下代码中HouseBook类的定义以及图 1.20 中显示的 UML 表示。

图 1.20. 非公共类Book不能在包library外部访问。

package building;
class House {}
package library;
class Book {}

根据当前的类定义,House类无法访问Book类。你能通过访问修饰符(在术语上)进行必要的更改,使Book类对House类可访问吗?

这一点不应该很难。从第 1.1 节中关于类声明的讨论中,你知道顶层类只能通过使用public或默认访问修饰符来定义。如果你使用访问修饰符public声明Book类,它将在定义它的包外部可访问。

注意

顶层类是指不在任何其他类中定义的类。在另一个类中定义的类称为嵌套内部类。嵌套和内部类不在 OCA Java SE 8 程序员 I 考试范围内。

它们控制什么?

访问修饰符通过其他类和接口(在同一包或不同包中)控制类或接口的可访问性,包括其成员(方法和变量)。通过使用适当的访问修饰符,您可以限制对类或接口及其成员的访问。

访问修饰符可以应用于所有类型的 Java 实体吗?

访问修饰符可以应用于类、接口及其成员(实例和类变量和方法)。局部变量和方法参数不能使用访问修饰符定义。尝试这样做将阻止代码编译。

有多少种访问修饰符:三种还是四种?

程序员经常对 Java 中的访问修饰符数量感到困惑,因为默认访问不是使用显式关键字定义的。如果一个 Java 类、接口、方法或变量没有使用显式访问修饰符定义,那么它被认为是使用默认访问定义的,也称为包访问

Java 有四个访问级别:

  • public(最不限制)

  • protected

  • 默认

  • private(最限制)

为了理解所有这些访问级别,我们将使用相同的类集:BookCourseBookLibrarianStoryBookHouse。图 1.21 使用 UML 表示法描述了这些类。

图 1.21. 一组类及其关系,以帮助您理解访问修饰符

BookCourseBookLibrarian 定义在包 library 中。类 StoryBookHouse 定义在包 building 中。此外,类 StoryBookCourseBook(在单独的包中定义)扩展了类 Book。使用这些类,我将展示类的可访问性和其成员在不同访问修饰符下的变化,从无关类到派生类,跨包。

在介绍每个访问修饰符时,我将向类 Book 添加一组实例变量和具有相关访问修饰符的方法。然后,在其它类中定义代码以访问类 Book 和其成员。

1.4.2. 公共访问修饰符

这是最不限制的访问修饰符。使用 public 访问修饰符定义的类和接口可以在所有包中访问,从派生类到无关类。

为了理解 public 访问修饰符,让我们将类 Book 定义为一个 public 类,并向其添加一个 public 实例变量(isbn)和一个 public 方法(printBook)。图 1.22 展示了 UML 表示法。

图 1.22. 理解 public 访问修饰符

Book 的定义:

public 访问修饰符被认为是限制最少的,因此让我们尝试从类 House 访问 publicBook 和其 public 成员。我们将使用类 House,因为 HouseBook 定义在不同的包中,并且它们是 无关的

注意

本章中提到的“无关类”指的是没有共享继承关系的类。例如,如果 House 既不派生自 BookBook 也不派生自 House,则类 HouseBook 是无关的。

House 在定义在同一个包中或作为派生类的情况下没有获得任何优势。

这是类 House 的代码:

在前面的例子中,类 Book 和其 public 成员——实例变量 isbn 和方法 printBook——对类 House 可访问。它们也对其他类可访问:StoryBookLibrarianHouseCourseBook。图 1.23 展示了可以访问 public 类及其成员的类。

图 1.23. 可以访问公共类及其成员的类

1.4.3. 受保护的访问修饰符

使用 protected 访问修饰符定义的类的成员可被访问

  • 同一包中定义的类和接口

  • 所有派生类,即使它们定义在单独的包中

让我们在类Book中添加一个protected实例变量author和一个方法modifyTemplate。图 1.24 显示了类的表示。

图 1.24. 理解protected访问修饰符

下面是类Book的代码(我故意省略了其public成员,因为在本节中它们不是必需的):

图 1.25 展示了来自同一包和不同包的类、派生类以及无关类如何访问类Book及其protected成员。

图 1.25. 从同一包和不同包中的无关和派生类访问类Book及其protected成员

House由于尝试访问方法modifyTemplate和变量author而无法编译。以下为编译错误信息:

House.java:8: modifyTemplate() has protected access in library.Book
        book.modifyTemplate();
            ^
注意

Java 代码由于语法错误而无法编译。在这种情况下,Java 编译器会通过行号和错误简短描述来通知有问题的代码。前面的代码是编译过程输出的。本书使用命令提示符来编译所有 Java 代码。

一个派生类无论这些成员定义在哪个包中,都会继承其基类的protected成员。

注意,派生类CourseBookStoryBook继承了类Bookprotected成员变量author和方法modifyTemplate()。如果类StoryBook尝试使用引用变量实例化Book,然后尝试访问其protected变量author和方法modifyTemplate(),则无法编译:

考试技巧

一种简洁但又不失简单的表述前述规则的方式是:一个派生类可以继承并访问其基类中的protected成员,无论这些成员定义在哪个包中。一个位于单独包中的派生类不能通过引用变量访问其基类的protected成员。

图 1.26 显示了可以访问类或接口protected成员的类。

图 1.26. 可以访问protected成员的类

1.4.4. 默认访问(包访问)

没有使用任何显式访问修饰符定义的类的成员具有包访问性(也称为默认访问性)。具有包访问的成员仅对定义在同一包中的类和接口是可访问的。默认访问也被称为包私有。将包想象成你的家,类想象成房间,房间里的东西想象成具有默认访问性的变量。这些东西并不局限于一个房间——它们可以访问你家中所有房间。但它们仍然属于你的家——你不会希望它们被家外的人访问。同样,当你定义一个包时,你可能希望使类中的成员对所有同一包中的其他类都是可访问的。

注意

尽管包私有访问权限与其他访问级别一样有效,但在实际项目中,它通常是由于经验不足的开发者忘记指定 Java 组件的访问模式而出现的结果。

让我们在 Book 类中定义一个具有默认访问权限的实例变量 issueCount 和一个方法 issueHistory。图 1.27 显示了包含这些新成员的类表示。

图 1.27。理解默认访问权限的类表示

图片描述

这是 Book 类的代码(我故意省略了它的 publicprotected 成员,因为在本节中不需要它们):

图片描述

您可以看到来自同一包和不同包的类、派生类以及无关类如何访问 Book 类及其成员(变量 issueCount 和方法 issueHistory),如图 1.28 所示。

图 1.28。来自同一包和不同包、派生类以及无关类的默认访问权限成员对 Book 类的访问

图片描述

因为 CourseBookLibrarian 类与 Book 类定义在同一个包中,所以它们可以访问 issueCountissueHistory 变量。因为 HouseStoryBook 类没有与 Book 类位于同一个包中,所以它们无法访问 issueCountissueHistory 变量。StoryBook 类抛出以下编译错误消息:

StoryBook.java:6: issueHistory() is not public in library.Book; cannot be accessed from outside package
        book.issueHistory();
            ^

HouseissueHistory() 的存在一无所知——它将因以下错误消息而无法编译:

House.java:9: cannot find symbol
symbol  : method issueHistory()
location: class building.House
        issueHistory();
定义具有默认访问权限的类 Book

如果我们定义一个具有默认访问权限的类会发生什么?如果该类本身具有默认(包)访问权限,其成员的可访问性会发生什么变化?

考虑这种情况:假设 Superfast Burgers 在一个美丽的岛屿上开设了一家新分店,并向来自世界各地的所有人提供免费餐点,这显然包括岛屿居民。但岛屿无法通过任何方式(空中和水路)进入。对于不居住在岛屿上的人来说,对这家特定的 Superfast Burgers 分店的意识有意义吗?这个例子的说明如图 1.29 所示。

图 1.29。由于岛屿无法通过空中和水路进入,因此 Superfast Burgers 无法从岛屿外部访问。

图片描述

岛屿在 Java 中就像一个包,Superfast Burgers 就像使用默认访问权限定义的类。就像 Superfast Burgers 无法从它存在的岛屿外部访问一样,具有默认(包)访问权限的类只能在定义它的包内部可见和可访问。它无法从其所在的包外部访问。

让我们重新定义具有默认(包)访问权限的类 Book,如下所示:

图片描述

对于定义在同一个包中的类 CourseBookLibrarian,类 Book 的行为保持不变。但是,类 Book 不能被位于单独包中的类 HouseStoryBook 访问。

让我们从类 House 开始。检查以下代码:

图片 1

House 生成以下编译错误信息:

House.java:2: library.Book is not public in library; cannot be accessed from outside package
import library.Book;

这是类 StoryBook 的代码:

图片 1

图 1.30 图 1.30 显示了哪些类可以访问具有默认(包)访问权限的类或接口的成员。

图 1.30. 可以访问默认(包)访问成员的类

图片 1

由于许多程序员对使用 protected 和默认访问修饰符使哪些成员变得可访问感到困惑,考试提示提供了一个简单而有趣的规则来帮助您记住它们之间的区别。

考试提示

默认访问可以与包私有(仅限于包内访问)相比较,而 protected 访问可以与包私有 + 子类(“子类”指的是派生类)相比较。子类只能通过继承而不是通过引用(通过在对象上使用点操作符访问成员)来访问 protected 方法。

1.4.5. 私有访问修饰符

private 访问修饰符是最具限制性的访问修饰符。使用 private 访问修饰符定义的类的成员只能由自身访问。无论相关的类或接口是否来自另一个包或扩展了该类,private 成员在其定义的类之外都是不可访问的。private 成员只能由定义它们的类和接口访问。

让我们通过向类 Book 添加一个 private 方法 countPages 来看看这个行为。图 1.31 图 1.31 描述了使用 UML 的类表示。

图 1.31. 理解 private 访问修饰符

图片 1

检查以下 Book 类的定义:

图片 1

在任何包中定义的任何类(无论是否派生)都不能访问 private 方法 countPages。但让我们尝试从类 CourseBook 中访问它。我选择 CourseBook 是因为这两个类都在同一个包中定义,并且 CourseBook 扩展了类 Book。以下是 CourseBook 的代码:

图片 1

因为类 CourseBook 尝试访问类 Book 的私有成员,所以它无法编译。同样,如果其他任何类(StoryBookLibrarianHouseCourse-Book)尝试访问类 Bookprivate 方法 countPages(),它也无法编译。

这里有一个有趣的情况:你认为 Book 实例能否使用引用变量来访问其私有成员吗?以下代码无法编译——尽管变量 b1 的类型是 Book,但它试图在 Book 之外访问其私有方法 countPages

图片 1

图 1.32 展示了可以访问类 private 成员的类。

图 1.32. 没有类可以访问另一个类的 private 成员

注意

对于你的真实项目,确实可以使用 Java 反射 来访问类外部的私有成员。但 Java 反射不在考试范围内。所以在回答关于私有成员可访问性的问题时,不要考虑它。

1.4.6. 访问修饰符和 Java 实体

每个访问修饰符都可以应用于所有 Java 实体吗?简单的答案是 。 表 1.3 列出了 Java 实体及其可使用的访问修饰符。

表 1.3. Java 实体及其可应用的访问修饰符
实体名称 public protected private
顶级类、接口、枚举 χ χ
类变量和方法
实例变量和方法
方法参数和局部变量 χ χ χ

如果你尝试在 表 1.3 中为 X 编写组合,会发生什么?这些组合中的任何一个都无法编译。以下是代码:

在考试中要注意这些组合。在任何一个代码片段中插入这些小而无效的组合,仍然会让你相信你正在接受关于线程或并发等相对复杂主题的测试。

考试技巧

注意 Java 实体和访问修饰符的无效组合。这样的代码无法编译。

故事转折 1.4

以下任务分配给了一组程序员:“如何在包 building 中声明一个类 Curtain,使其在包 building 外不可见?”

这些是 Paul、Shreya、Harry 和 Selvan 提交的答案。你认为哪个是正确的,为什么?(你可以在附录中检查你的故事转折答案。)

| 程序员姓名 | 提交的代码 |
| --- | --- | --- | --- |
| Paul | 包 building; 公共类 Curtain {} |
| Shreya | 包 building; 受保护的类 Curtain {} |
| Harry | 包 building; 类 Curtain {} |
| Selvan | 包 building; 私有类 Curtain {} |

你的职位可能赋予你特殊的权限或责任。例如,如果你是一名 Java 开发者,你可能需要更新你的编程技能或在 Java 方面获得专业认证。同样,你可以通过使用 非访问修饰符 来赋予你的 Java 实体特殊的权限、责任和行为,这些内容将在下一节中介绍。

1.5. 非访问修饰符

[7.5] 使用抽象类和接口

[6.2] 将静态关键字应用于方法和字段

本节讨论非访问修饰符 abstractfinalstatic。访问修饰符控制类及其成员在类外和包外的可访问性。非访问修饰符改变 Java 类及其成员的默认行为。

例如,如果你将 abstract 关键字添加到类的定义中,它就不能被实例化。这就是非访问修饰符的神奇之处。

你可以用以下非访问修饰符来描述你的类、接口、方法和变量(尽管并非所有都适用于每个 Java 实体):

  • abstract

  • static

  • final

  • synchronized

  • native

  • strictfp

  • transient

  • volatile

OCA Java SE 8 程序员 I 考试仅涵盖这些非访问修饰符中的三个:abstractfinalstatic,这些内容我将详细讲解。为了避免对其他修饰符的混淆,我在这里简要描述它们:

  • synchronized—一个 synchronized 方法不能被多个线程同时访问。你不能用这个修饰符标记类、接口或变量。

  • native—一个 native 方法调用并使用其他编程语言(如 C 或 C++)中实现的库和方法。你不能用这个修饰符标记类、接口或变量。

  • transient—当相应的对象被序列化时,transient 变量不会被序列化。transient 修饰符不能应用于类、接口或方法。

  • volatile—一个 volatile 变量的值可以被不同的线程安全地修改。类、接口和方法不能使用这个修饰符。

  • strictfp—使用此关键字定义的类、接口和方法确保使用浮点数进行的计算在所有平台上都是相同的。此修饰符不能与变量一起使用。

现在我们来看看考试中的三个非访问修饰符。

1.5.1. 抽象修饰符

当添加到类、接口或方法的定义中时,abstract 修饰符会改变其默认行为。因为它是一个非访问修饰符,所以 abstract 不会改变类、接口或方法的可访问性。

让我们通过 abstract 修饰符来检查每个这些行为的特性。

抽象类

abstract 关键字作为具体类定义的前缀时,即使该类没有定义任何抽象方法,它也会将其转换为抽象类。以下代码是一个有效的抽象类示例:

abstract class Person {
    private String name;
    public void displayName() { }
}

抽象类不能被实例化,这意味着以下代码将无法编译:

这是前一个类抛出的编译错误:

University.java:4: Person is abstract; cannot be instantiated
    Person p = new Person();
               ^
1 error
考试技巧

一个抽象类可以或不可以定义一个抽象方法。但一个具体类不能定义一个抽象方法。

抽象接口

接口默认是一个抽象实体。Java 编译器会自动将抽象关键字添加到接口的定义中。因此,将抽象关键字添加到接口的定义中是多余的。以下接口的定义是相同的:

图片

抽象方法

一个抽象方法没有主体。通常,抽象方法由派生类实现。以下是一个例子:

图片

考试技巧

一个空体的方法不是抽象方法。

抽象变量

不同的变量类型(实例、静态、局部和方法的参数)都不能定义为抽象

考试技巧

不要被试图将非访问修饰符抽象应用于变量的代码所欺骗。这样的代码无法编译。

1.5.2. final 修饰符

关键字final可以与类、变量或方法的声明一起使用。它不能与接口的声明一起使用。

final 类

被标记为final的类不能被另一个类扩展。如果Person类被标记为final,则Professor类将无法编译,如下所示:

图片

final 接口

接口不能标记为final。接口默认是抽象的,标记为final将阻止你的接口编译:

图片

final 变量

final变量不能重新赋值。它只能赋值一次。以下代码说明:

图片

将前面的例子与以下尝试重新赋值给 final 变量的代码进行比较:

图片

容易将向final变量重新赋值与在final变量上调用方法混淆,这可能会改变它所引用的对象的状态。如果引用变量被定义为final变量,你不能将其重新赋值为另一个对象,但可以调用此变量的方法(这些方法会修改其状态):

图片

final 方法

在基类中定义的final方法不能被派生类重写。检查以下代码:

图片

如果派生类中的方法与其基类的方法具有相同的签名,则称为重写方法。重写方法将与多态一起在第六章中讨论。

1.5.3. 静态修饰符

非访问修饰符静态可以应用于变量、方法、类和接口的声明。我们将在接下来的章节中逐一考察它们。

静态变量

静态变量属于一个类。它们对所有类的实例都是通用的,并不属于任何单个实例。静态属性独立于类的任何实例存在,即使没有创建类的实例也可以访问。你可以将静态变量与共享变量进行比较。一个静态变量被类中的所有对象共享。

注意

一个类和一个接口都可以声明 static 变量。本节涵盖了在类中定义的 static 变量的声明和使用。第六章 详细介绍了接口及其 static 变量。

static 变量想象成一个由组织中的员工共享的公共银行保险库。每个员工都可以访问相同的银行保险库,因此一个员工所做的任何更改都会对所有其他员工可见,如 图 1.33 所示。

图 1.33. 比较共享银行保险库与 static 变量

图 1.34 定义了一个 Emp 类,该类定义了一个非 static 变量 name 和一个 static 变量 bankVault

图 1.34. Emp 类的定义,包含 static 变量 bankVault 和非 static 变量 name

现在是测试我们到目前为止所讨论内容的时候了。下面的 TestEmp 类创建了 Emp 类(来自 图 1.34)的两个对象,并使用这些单独的对象修改变量 bankVault 的值:

在前面的代码示例中,emp1.bankVaultemp2.bankVaultEmp.bank-Vault 都引用了相同的 static 属性:bankVault

考试技巧

尽管你可以使用对象引用变量来访问 static 成员,但这样做并不建议。因为 static 成员属于类而不是单个对象,使用对象引用变量来访问 static 成员可能会使它们看起来属于一个对象。访问它们的最佳方式是使用类名。staticfinal 非访问修饰符可以一起使用来定义 常量(值不能改变的变量)。

在以下代码中,类 Emp 定义了常量 MIN_AGEMAX_AGE

虽然你可以将常量定义为非 static 成员,但通常的做法是将常量定义为 static 成员,因为这样做可以使常量值在对象和类之间共享。

static 方法

static 方法与对象无关,不能使用类的任何实例变量。你可以定义 static 方法来访问或操作 static 变量:

使用 static 方法定义 实用方法 是一种常见的做法,这些方法是通常操作方法参数以计算并返回适当值的函数:

static double interest(double num1, double num2, double num3) {
    return(num1+num2+num3)/3;
}

下面的实用 (static) 方法没有定义输入参数。方法 averageOfFirst100Integers 计算并返回数字 1100 的平均值:

非私有 static 变量和方法会被派生类继承。static 成员不参与运行时多态。你无法在派生类中重写 static 成员,但可以重新定义它们。

如果你不知道继承和派生类,关于静态方法和它们行为的讨论可能会相当令人困惑。但如果你不理解所有这些内容,请不要担心。我将在第六章中介绍派生类和继承。现在,请注意,静态方法可以使用对象引用变量的名称和类名以类似于静态变量的方式访问。

静态方法可以访问什么?

无论是静态方法还是静态变量都不能访问类的非静态变量和方法。但反之则不然:非静态变量和方法可以访问静态变量和方法,因为类的静态成员即使没有类的实例也存在。静态成员不允许访问实例方法或变量,这些方法或变量只有当创建了类的实例时才存在。

检查以下代码:

这是上一个类抛出的编译错误:

MyClass.java:3: nonstatic method count() cannot be referenced from a static context
    static int x = count();
                   ^
1 error

以下代码是有效的:

考试技巧

静态方法和变量不能访问类的实例成员。

表 1.4 总结了静态和非静态成员的访问能力。

表 1.4. 静态和非静态成员的访问能力
成员类型 是否可以访问静态属性或方法? 是否可以访问非静态属性或方法?
静态
非静态
从空引用访问静态成员

因为静态变量和方法属于类而不是实例,所以你可以使用初始化为null的变量来访问它们。在考试中要注意这类问题。这样的代码不会抛出运行时异常(确切地说,是NullPointerException)。在以下示例中,引用变量emp被初始化为null

考试技巧

你可以使用空引用来访问静态变量和方法。

静态类和接口

认证考生经常询问有关静态类和接口的问题,所以我会快速在本节中介绍这些内容,以消除与它们相关的任何混淆。但请注意,静态类和接口是嵌套类和接口的类型,这些类型不包括在 OCA Java 8 程序员 I 考试范围内。

你不能使用关键字static来前缀顶级类或接口的定义。顶级类或接口是在另一个类或接口之外定义的。以下代码将无法编译:

static class Person {}
static interface MyInterface {}

但你可以定义一个类和一个接口作为另一个类的静态成员。以下代码是有效的:

下一节将介绍导致 Java 在二十年前流行,并且至今仍具有强大影响力的 Java 特点。

1.6. Java 的特点和组件

[1.5] 比较和对比 Java 的功能和组件,例如:平台独立性、面向对象、封装等。

Java 编程语言于 1995 年发布。它主要开发用于与消费电子产品协同工作。但很快它就因为与网络浏览器的结合而变得非常流行,用于提供动态内容(使用小程序),这不需要为不同的平台重新编译。让我们开始了解 Java 的独特特性和组件,这些特性仍然使它成为一种流行的编程语言。

注意

考试将询问与 Java 相关或无关的功能和组件。

1.6.1. Java 的有效功能和组件

Java 相比其他语言和平台提供了多项优势。

平台独立性

这个特性是 Java 自发布以来取得惊人增长的主要原因之一。它也被称作“一次编写,到处运行”(WORA)——这是 Sun Microsystems^(TM) 创造的一个口号,用以强调 Java 的平台独立性。

Java 代码可以在多个系统上执行,而无需重新编译。Java 代码被编译成 字节码,由 虚拟机——Java 虚拟机(JVM)执行。JVM 安装在不同的操作系统平台上,如 Windows、Mac 或 Linux。JVM 将字节码解释为特定于机器的指令以执行。JVM 的实现细节取决于机器,可能在不同平台上有所不同,但它们都以类似的方式解释相同的字节码。Java 编译器生成的字节码被所有带有 JVM 的平台支持。

其他流行的编程语言,如 C 和 C++,会将它们的代码编译到宿主系统中。因此,代码必须为不同的平台重新编译。

面向对象

Java 模拟现实生活中的对象定义和行为。在现实生活中,状态和行为与对象相关联。同样,所有 Java 代码都是在类、接口或枚举中定义的。你需要创建它们的对象来使用它们。

抽象

Java 允许你抽象对象,并在代码中只包含所需的属性和行为。例如,如果你正在开发一个跟踪一个国家人口的应用程序,你会记录一个人的姓名、地址和联系详情。但对于一个健康跟踪系统,你可能还想包括与健康相关的细节和行为。

封装

使用 Java 类,你可以封装一个对象的状态和行为。类的状态或字段受到不受欢迎的访问和操作的防护。你可以控制对对象访问和修改的级别。

继承

Java 允许其类继承其他类并实现接口。接口可以继承其他接口。这可以节省你重新定义通用代码的时间。

多态

多态的直译意思是“多种形式”。Java 允许其类的实例对同一方法调用表现出多种行为。你将在第六章中详细了解这一点。章节链接。

类型安全

在 Java 中,你必须在使用变量之前声明其数据类型 before 你才能使用它。这意味着你有一个编译时检查,确保你永远不会将错误类型的值赋给变量。

自动内存管理

与其他编程语言如 C 或 C++ 不同,Java 使用垃圾回收器进行自动内存管理。它们从不再使用的对象中回收内存。这使开发者免于显式管理内存。它还防止了内存泄漏。

多线程和并发

Java 自首次发布以来就支持多线程和并发——由其核心 API 中定义的类和接口提供支持。

安全性

Java 包含多个内置的安全功能(尽管本考试并未涵盖所有内容),以控制对您资源的访问和程序的执行。

Java 是类型安全的,并包括垃圾回收功能。它提供安全的类加载,并且验证确保执行的是合法的 Java 代码。

Java 平台定义了多个 API,包括加密和公钥基础设施。在安全管理器控制下运行的 Java 应用程序可以控制对您资源的访问,如读取或写入文件。可以通过策略文件来控制对资源的访问。Java 允许您定义数字签名、证书和密钥库以保护代码和文件交换。已签名的代码用于执行。

Java 通过封装和数据隐藏等特性,确保了其对象的状态安全。Java 小程序在浏览器中执行,不允许代码下载到系统中,从而为浏览器及其运行系统提供了安全性。

1.6.2. Java 的无关特性和组件

考试可能还会包括一些无关的术语。

单线程

Java 支持使用内置的类和接口进行多线程编程。你可以创建和使用单线程,但 Java 语言本身不是单线程的。即使你创建了执行的单线程,Java 也会在单独的线程中执行自己的进程,如垃圾回收。Java 不是一个单线程的语言。

与 JavaScript 相关

Java 与 JavaScript(除了名称相似之外)无关。JavaScript 是一种用于网页的编程语言,用于使网页具有交互性。

1.7. 摘要

本章从查看 Java 类的结构开始。尽管你应该知道如何使用 Java 类、Java 源代码文件(.java 文件)和 Java 字节码文件(.class 文件),但 OCA Java SE 8 程序员 I 考试只会就前两者的结构和组件进行提问——即类和源代码,而不是 Java 字节码。

我们讨论了 Java 类和 Java 源代码文件的组件。一个类可以定义多个组件,即importpackage语句、变量、构造函数、方法、注释、嵌套类、嵌套接口、注解和枚举。Java 源代码文件(.java)可以定义多个类和接口。

然后,我们介绍了可执行和非可执行 Java 类之间的差异和相似之处。一个可执行 Java 类定义了 JVM 启动执行时的入口点(main方法)。main方法应该使用所需的方法签名定义;否则,该类将无法被分类为可执行 Java 类。

包用于将相关的类和接口分组在一起。它们还提供访问保护和命名空间管理。import语句用于从其他包中导入类和接口。如果没有import语句,类和接口应该通过它们的完全限定名(完整的包名加上类或接口名)来引用。

访问修饰符控制类及其成员在包内和包之间的访问。Java 定义了四个访问修饰符:publicprotected、默认和private。当默认访问被分配给一个类或其成员时,它前面没有前缀。没有访问修饰符的缺失等于将类或其成员分配为默认访问。最不限制的访问修饰符是public,而private是最限制的。protected访问位于public和默认访问之间,允许包外派生类访问。

我们介绍了abstractstatic非访问修饰符。一个类或一个方法可以被定义为abstract成员。abstract类不能被实例化。方法和变量可以被定义为static成员。一个类的所有对象共享相同的static变量副本,这些变量也被称为类级别变量。

最后,我们介绍了 Java 的特性和组件,使其成为流行的选择。

1.8. 复习笔记

本节列出了本章涵盖的主要要点。

Java 类和源代码文件的结构:

  • OCA Java SE 8 程序员 I 考试涵盖了 Java 类和 Java 源代码文件(.java 文件)的结构和组件。它不涵盖 Java 字节码文件(.class 文件)的结构和组件。

  • 一个类可以定义多个组件。你听说过的所有 Java 组件都可以在 Java 类中定义:importpackage语句、变量、构造函数、方法、注释、嵌套类、嵌套接口、注解和枚举。

  • 本考试不涵盖嵌套类、嵌套接口、注解和枚举的定义。

  • 如果一个类定义了package语句,它应该是类定义中的第一个语句。

  • package语句不能出现在类声明内或类声明之后。

  • 如果存在,package语句应该在一个类中恰好出现一次。

  • import语句允许使用简单名称、类的非限定名称和接口。

  • 不能使用import语句导入具有相同名称的多个类或接口。

  • 一个类可以包含多个import语句。

  • 如果一个类包含package语句,所有的import语句都应该跟在package语句之后。

  • 如果存在,import语句必须放在任何类或接口定义之前。

  • 注释是类的另一个组成部分。注释用于注释 Java 代码,并且可以在类中多个位置出现。

  • 注释可以出现在package语句之前或之后,类定义之前或之后,以及方法定义之前、之内或之后。

  • 注释有两种类型:多行注释和行尾注释。

  • 注释可以包含任何特殊字符(包括 Unicode 字符集中的字符)。

  • 多行注释跨越多行代码。它们以/*开始,以*/结束。

  • 行尾注释以//开始,正如其名所示,放置在代码行或空白行的末尾。//和行尾之间的文本被视为注释。

  • 类声明和类定义是 Java 类的一部分。

  • 一个 Java 类可以定义零个或多个实例变量、方法和构造函数。

  • 在类中,实例变量、构造函数和方法的定义顺序无关紧要。

  • 一个类可以在定义方法之前或之后定义实例变量,并且仍然可以使用它。

  • 一个 Java 源代码文件(.java 文件)可以定义多个类和接口。

  • 只能在与源代码文件同名的文件中定义public类。

  • packageimport语句适用于同一源代码文件(.java 文件)中定义的所有类和接口。

可执行的 Java 应用程序:

  • 可执行的 Java 类是当传递给 Java 虚拟机(JVM)时,在类中的特定点开始执行其执行的类。这个执行点是main方法。

  • 为了使一个类可执行,该类应该定义一个签名为public static void main(String args[])public static void main(String... args)main方法。staticpublic的位置可以互换,方法参数可以使用任何有效的名称。

  • 一个类可以定义多个名为main的方法,只要这些方法的签名与前面提到的main方法的签名不匹配。这些重载版本不被认为是main方法。

  • main方法接受一个类型为String的数组,包含 JVM 传递给它的方法参数。

  • 关键字java和类的名称不会被作为命令参数传递给main方法。

Java 包:

  • 您可以使用包将相关的一组类和接口组合在一起。

  • 默认情况下,不同包和子包中的所有类和接口对彼此不可见。

  • 包和子包名称使用点分隔。

  • 同一包中的所有类和接口对彼此可见。

  • import 语句允许使用其他包中定义的包装类和接口的简单名称。

  • 您不能使用 import 语句访问来自不同包的具有相同名称的多个类或接口。

  • 您可以使用 import 语句导入包的单个成员或所有成员(类和接口)。

  • 您不能在 import 语句中使用通配符字符(星号 *)导入子包中的类。

  • 来自默认包的类不能用于任何命名包装类,无论它是否定义在同一目录中。

  • 您可以使用 static import 语句导入类的单个 static 成员或所有 static 成员。

  • 在类中不能将 import 语句放在 package 语句之前。任何尝试这样做都会导致类编译失败。

  • 默认包的成员只能被定义在同一系统目录上的类或接口访问。

Java 访问修饰符:

  • 访问修饰符控制类及其成员在类和包外部可访问性。

  • Java 定义了四个访问级别:publicprotected、默认和private

  • Java 定义了三个访问修饰符:publicprotectedprivate

  • public 访问修饰符是最不限制的访问修饰符。

  • 使用 public 访问修饰符定义的类及其成员对定义它们的包中的相关和不相关的类都是可访问的。

  • 使用 protected 访问修饰符定义的类的成员对同一包中定义的类和接口以及所有派生类都是可访问的,即使它们定义在不同的包中。

  • 未使用显式访问修饰符定义的类的成员具有包访问性(也称为默认访问性)。

  • 具有包访问的成员只能被定义在同一包中的类和接口访问。

  • 使用默认访问定义的类在其包外部不可访问。

  • 使用 private 访问修饰符定义的类的成员只能在其定义的类中访问。无论相关的类或接口是否来自另一个包或扩展了该类,这都不重要。私有成员在其定义的类外部不可访问。

  • private 访问修饰符是最限制的访问修饰符。

非访问修饰符:

  • 非访问修饰符改变 Java 类及其成员的默认属性。

  • 本考试涵盖的非访问修饰符有 abstractfinalstatic

  • abstract 关键字加在具体类的定义之前时,即使它没有定义任何 abstract 方法,也可以将其转换为 abstract 类。

  • abstract 类不能被实例化。

  • 接口是隐式 abstract 的。Java 编译器自动将 abstract 关键字添加到接口的定义中(这意味着在接口定义中添加 abstract 关键字是多余的)。

  • abstract 方法没有主体。当一个非 abstract 类扩展了一个具有 abstract 方法的类时,它必须实现该方法。

  • 变量不能定义为 abstract 变量。

  • static 修饰符可以应用于内部类、内部接口、变量和方法。内部类和接口不包含在本考试中。

  • 方法不能同时定义为 abstractstatic

  • static 属性(字段和方法)对所有类的实例都是通用的,并不特定于类的任何实例。

  • static 属性独立于类的任何实例存在,即使没有创建类的实例也可以访问。

  • static 属性也被称为 类字段类方法,因为它们被认为属于它们的类,而不是属于该类的任何实例。

  • 可以使用引用对象变量的名称或类的名称来访问 static 变量或方法。

  • static 方法或变量不能访问类的非 static 变量或方法。但反之亦然:非 static 变量和方法可以访问 static 变量和方法。

  • static 类和接口是一种嵌套类和接口,但它们不包含在本考试中。

  • 不能用 static 关键字前缀顶级类或接口的定义。顶级类或接口是在另一个类或接口之外定义的。

Java 的特性和组件:

  • 面向对象— Java 模拟现实生活中的对象定义和行为。它使用类、接口或枚举来定义所有代码。

  • 抽象— Java 允许你抽象对象,并在代码中只包含所需的属性和行为。

  • 封装— 类的状态或字段受到不受欢迎的访问和操作的防护。

  • 继承— Java 允许其类继承其他类并实现接口。接口可以继承其他接口。

  • 多态— Java 允许其类的实例对同一方法调用表现出多种行为。

  • 类型安全— 在 Java 中,在使用变量之前必须声明其数据类型。

  • 自动内存管理— Java 使用垃圾回收器进行自动内存管理。它们从不再使用的对象中回收内存。

  • 多线程和并发— Java 定义了类和接口,以使开发者能够开发多线程代码。

  • Java 不是一个单线程语言。

1.9. 样例考试问题

Q1-1.

给定:

class EJava {
    //..code
}

以下哪个选项可以编译?

  1. package java.oca.associate;
    class Guru {
        EJava eJava = new EJava();
    }
    
  2. package java.oca;
    import EJava;
    class Guru {
        EJava eJava;
    }
    
  3. package java.oca.*;
    import java.default.*;
    class Guru {
        EJava eJava;
    }
    
  4. package java.oca.associate;
    import default.*;
    class Guru {
        default.EJava eJava;
    }
    
  5. 以上皆非

Q1-2.

以下编号的 Java 类组件列表没有特定的顺序。选择它们在任何 Java 类中出现的可接受顺序(选择所有适用的):

  1. 注释
  2. import语句
  3. package语句
  4. 方法
  5. 类声明
  6. 变量
  7. 1, 3, 2, 5, 6, 4
  8. 3, 1, 2, 5, 4, 6
  9. 3, 2, 1, 4, 5, 6
  10. 3, 2, 1, 5, 6, 4

Q1-3.

以下哪个示例定义了正确的 Java 类结构?

  1. #connect java compiler;
    #connect java virtual machine;
    class EJavaGuru {}
    
  2. package java compiler;
    import java virtual machine;
    class EJavaGuru {}
    
  3. import javavirtualmachine.*;
    package javacompiler;
    class EJavaGuru {
        void method1() {}
        int count;
    }
    
  4. package javacompiler;
    import javavirtualmachine.*;
    class EJavaGuru {
        void method1() {}
        int count;
    }
    
  5. #package javacompiler;
    $import javavirtualmachine;
    class EJavaGuru {
        void method1() {}
        int count;
    }
    
  6. package javacompiler;
    import javavirtualmachine;
    Class EJavaGuru {
        void method1() {}
        int count;
    }
    

Q1-4.

给定以下 Java 源代码文件 MyClass.java 的内容,选择正确的选项:

// contents of MyClass.java
package com.ejavaguru;
import java.util.Date;
class Student {}
class Course {}
  1. 导入的类java.util.Date只能在Student类中访问。
  2. 导入的类java.util.Date可以被StudentCourse类访问。
  3. StudentCourse两个类都定义在com.ejava-guru包中。
  4. 只有Student类定义在com.ejavaguru包中。Course类定义在默认的 Java 包中。

Q1-5.

给定以下类EJavaGuru的定义,

class EJavaGuru {
    public static void main(String[] args) {
        System.out.println(args[1]+":"+ args[2]+":"+ args[3]);
    }
}

如果使用以下命令执行EJavaGuru,它的输出是什么?

java EJavaGuru one two three four
  1. one:two:three
  2. EJavaGuru:one:two
  3. java:EJavaGuru:one
  4. two:three:four

Q1-6.

以下哪个选项,当插入到//INSERT CODE HERE处时,将打印出EJavaGuru

public class EJavaGuru {
    // INSERT CODE HERE
    {
        System.out.println("EJavaGuru");
    }
}
  1. public void main (String[] args)
  2. public void main(String args[])
  3. static public void main (String[] array)
  4. public static void main (String args)
  5. static public main (String args[])

Q1-7.

“一次编写,到处运行”的含义是什么?选择正确的选项:

  1. Java 代码可以由一个团队成员编写,并由其他团队成员执行。
  2. 仅用于营销目的。
  3. 它使 Java 程序能够一次性编译,并且可以在任何 JVM 上执行而无需重新编译。
  4. 旧 Java 代码在发布新版本的 JVM 时不需要重新编译。

Q1-8.

com.ejavaguru包中定义了一个Course类。考虑到相应的类文件物理位置是/mycode/com/ejavaguru/Course.class,并且执行发生在 mycode 目录内,以下哪行代码,当插入到// INSERT CODE HERE处时,将导入Course类到MyCourse类中?

// INSERT CODE HERE
class MyCourse {
    Course c;
}
  1. import mycode.com.ejavaguru.Course;
  2. import com.ejavaguru.Course;
  3. import mycode.com.ejavaguru;
  4. import com.ejavaguru;
  5. import mycode.com.ejavaguru*;
  6. import com.ejavaguru*;

Q1-9.

检查以下代码:

class Course {
    String courseName;
}
class EJavaGuru {
    public static void main(String args[]) {
        Course c = new Course();
        c.courseName = "Java";
        System.out.println(c.courseName);
    }
}

如果将变量courseName定义为private变量,以下哪个语句将是正确的?

  1. EJavaGuru将打印Java
  2. EJavaGuru将打印null
  3. EJavaGuru不会编译。
  4. EJavaGuru将在运行时抛出异常。

Q1-10.

给定以下Course类的定义,

package com.ejavaguru.courses;
class Course {
    public String courseName;
}

以下代码的输出是什么?

package com.ejavaguru;
import com.ejavaguru.courses.Course;
class EJavaGuru {
    public static void main(String args[]) {
        Course c = new Course();
        c.courseName = "Java";
        System.out.println(c.courseName);
    }
}
  1. EJavaGuru将打印Java
  2. EJavaGuru将打印null
  3. EJavaGuru无法编译。
  4. EJavaGuru将在运行时抛出异常。

Q1-11.

给定以下代码,选择正确的选项:

package com.ejavaguru.courses;
class Course {
    public String courseName;
    public void setCourseName(private String name) {
        courseName = name;
    }
}
  1. 你不能将方法参数定义为private变量。
  2. 方法参数应该使用public或默认可访问性定义。
  3. 对于重写的方法,方法参数应该使用protected可访问性定义。
  4. 以上都不对。

1.10. 样本考试问题的答案

Q1-1.

给定:

class EJava {
    //..code
}

以下哪个选项可以编译?

  1. package java.oca.associate;
    class Guru {
        EJava eJava = new EJava();
    }
    
  2. package java.oca;
    import EJava;
    class Guru {
        EJava eJava;
    }
    
  3. package java.oca.*;
    import java.default.*;
    class Guru {
        EJava eJava;
    }
    
  4. package java.oca.associate;
    import default.*;
    class Guru {
        default.EJava eJava;
    }
    
  5. 以上都不对

答案:e

解释:未在包中定义的类在 Java 中隐式地定义在默认包中。但这样的类不能被显式定义在包中的类或接口访问。

选项 a 是错误的。EJava类未在包中定义,因此无法被定义在java.oca.associate包中的Guru类访问。

选项 b、c 和 d 无法编译。选项 b 在import语句中使用了无效的语法。选项 c 和 d 尝试从不存在的包中导入类——java.defaultdefault

Q1-2.

以下编号的 Java 类组件列表没有特定的顺序。选择它们在 Java 类中出现的正确顺序(选择所有适用的):

  1. 注释
  2. import语句
  3. package语句
  4. 方法
  5. 类声明
  6. 变量
    1. 1, 3, 2, 5, 6, 4
    2. 3, 1, 2, 5, 4, 6
    3. 3, 2, 1, 4, 5, 6
    4. 3, 2, 1, 5, 6, 4

答案:a, b, d

解释:注释可以出现在类中的任何位置。它们可以出现在packageimport语句之前和之后。它们可以出现在类、方法或变量声明之前或之后。

类中第一个语句(如果存在)应该是package语句。它不能放在import语句或类声明之后。

import语句应该跟在package语句之后,并跟在类声明之后。

类声明跟在import语句之后(如果存在)。它后面跟着方法和变量的声明。

答案 c 是错误的。在类或接口定义之前不能定义任何变量或方法。

Q1-3.

以下哪个示例定义了正确的 Java 类结构?

  1. #connect java compiler;
    #connect java virtual machine;
    class EJavaGuru {}
    
  2. package java compiler;
    import java virtual machine;
    class EJavaGuru {}
    
  3. import javavirtualmachine.*;
    package javacompiler;
    class EJavaGuru {
        void method1() {}
        int count;
    }
    
  4. package javacompiler;
    import javavirtualmachine.*;
    class EJavaGuru {
        void method1() {}
        int count;
    }
    
  5. #package javacompiler;
    $import javavirtualmachine;
    class EJavaGuru {
        void method1() {}
        int count;
    }
    
  6. package javacompiler;
    import javavirtualmachine;
    Class EJavaGuru {
        void method1() {}
        int count;
    }
    

答案:d

解释:选项 a 是错误的,因为#connect在 Java 中不是一个语句。#在 UNIX 中用于添加注释。

选项 b 是错误的,因为包名(Java 编译器)不能包含空格。此外,java virtual machine不是一个有效的包名,不能在类中导入。要导入的包名不能包含空格。

选项 c 是错误的,因为(如果存在)package语句必须放在import语句之前。

选项 e 是错误的。#package$import 不是 Java 中的有效语句或指令。

选项 f 是错误的。Java 是区分大小写的,所以单词 class 与单词 Class 不相同。定义类的正确关键字是 class

Q1-4.

给定以下 Java 源代码文件 MyClass.java 的内容,选择正确的选项:

// contents of MyClass.java
package com.ejavaguru;
import java.util.Date;
class Student {}
class Course {}
  1. 导入的类 java.util.Date 只能在 Student 类中访问。
  2. 导入的类 java.util.Date 可以被 StudentCourse 类同时访问。
  3. StudentCourse 两个类都定义在 com.ejava-guru 包中
  4. 只有 Student 类在 com.ejavaguru 包中定义。Course 类在默认的 Java 包中定义。

答案:b, c

说明:您可以在一个 Java 源代码文件中定义多个类、接口和枚举。

选项 a 是错误的。import 语句适用于同一 Java 源代码文件中定义的所有类、接口和枚举。

选项 d 是错误的。如果源代码文件中定义了 package 语句,那么其中定义的所有类、接口和枚举都将存在于同一个 Java 包中。

Q1-5.

给定以下类 EJavaGuru 的定义,

class EJavaGuru {
    public static void main(String[] args) {
        System.out.println(args[1]+":"+ args[2]+":"+ args[3]);
    }
}

如果使用以下命令执行前面的类,它的输出是什么?

java EJavaGuru one two three four
  1. one:two:three
  2. EJavaGuru:one:two
  3. java:EJavaGuru:one
  4. two:three:four

答案:d

说明:传递给类 main 方法的命令行参数不包含单词 Java 和类的名称。

因为数组的位置是从零开始的,方法参数被分配以下值:

args[0] -> one

args[1] -> two

args[2] -> three

args[3] -> four

该类打印 two:three:four

Q1-6.

以下哪个选项插入到 //INSERT CODE HERE 处,将打印出 EJavaGuru

public class EJavaGuru {
    // INSERT CODE HERE
    {
        System.out.println("EJavaGuru");
    }
}
  1. public void main (String[] args)
  2. public void main(String args[])
  3. static public void main (String[] array)
  4. public static void main (String args)
  5. static public main (String args[])

答案:c

说明:选项 a 是错误的。此选项定义了一个有效的方法,但不是一个有效的 main 方法。main 方法应该定义为 static 方法,但选项 a 中的方法声明中缺少了这一点。

选项 b 是错误的。此选项与选项 a 中的方法类似,只有一个区别。在此选项中,方括号放置在方法参数名称之后。main 方法接受一个数组作为方法参数,要定义一个数组,方括号可以放置在数据类型或方法参数名称之后。

选项 c 是正确的。Java 编译器会忽略类中的额外空格。

选项 d 是错误的。main 方法接受一个 String 类型的数组作为方法参数。此选项中的方法接受一个单独的 String 对象。

选项 e 是错误的。这不是一个有效的方法定义,并且没有指定方法的返回类型。这一行代码将无法编译。

Q1-7.

“一次编写,到处运行”的含义是什么?选择正确的选项:

  1. Java 代码可以由一个团队成员编写,并由其他团队成员执行。
  2. 这只是为了营销目的。
  3. 它使 Java 程序能够一次编译,并且可以在任何 JVM 上执行,无需重新编译。
  4. 当发布新的 JVM 版本时,旧的 Java 代码不需要重新编译。

答案:c

解释:平台独立性,或“一次编写,到处运行”,使 Java 代码能够一次编译并在任何具有 JVM 的系统上运行。它不仅仅是为了营销目的。

Q1-8.

在包 com.ejavaguru 中定义了一个 Course 类。假设相应的类文件物理位置是 /mycode/com/ejavaguru/Course.class,并且执行发生在 mycode 目录中,以下哪一行代码,当插入到 // INSERT CODE HERE 时,将导入 Course 类到类 MyCourse 中?

// INSERT CODE HERE
class MyCourse {
    Course c;
}
  1. import mycode.com.ejavaguru.Course;
  2. import com.ejavaguru.Course;
  3. import mycode.com.ejavaguru;
  4. import com.ejavaguru;
  5. import mycode.com.ejavaguru*;
  6. import com.ejavaguru*;

答案:b

解释:选项 a 是错误的。定义包 com.ejavaguru 的基本目录 mycode 不应包含在 import 语句中。

选项 c 和 e 是错误的。在 import 语句中没有指定类的物理位置。

选项 d 和 f 是错误的。ejavaguru 是一个包。要导入一个包及其成员,包名后应跟 .*,如下所示:

import com.ejavaguru.*;

Q1-9.

检查以下代码:

class Course {
    String courseName;
}
class EJavaGuru {
    public static void main(String args[]) {
        Course c = new Course();
        c.courseName = "Java";
        System.out.println(c.courseName);
    }
}

如果将变量 courseName 定义为 private 变量,以下哪个陈述将是正确的?

  1. EJavaGuru 将打印 Java。
  2. EJavaGuru 将打印 null。
  3. EJavaGuru 不会编译。
  4. EJavaGuru 在运行时将抛出异常。

答案:c

解释:如果变量 courseName 被定义为 private 成员,它将无法从类 EJavaGuru 中访问。尝试这样做将在编译时导致失败。因为代码无法编译,所以无法执行。

Q1-10.

给定以下 Course 类的定义,

package com.ejavaguru.courses;
class Course {
    public String courseName;
}

以下代码的输出是什么?

package com.ejavaguru;
import com.ejavaguru.courses.Course;
class EJavaGuru {
    public static void main(String args[]) {
        Course c = new Course();
        c.courseName = "Java";
        System.out.println(c.courseName);
    }
}
  1. EJavaGuru 将打印 Java
  2. EJavaGuru 将打印 null
  3. EJavaGuru 将无法编译。
  4. EJavaGuru 在运行时将抛出异常。

答案:c

解释:该类将无法编译,因为非公共类不能在其定义的包外部访问。因此,类 Course 不能在类 EJavaGuru 内部访问,即使它被显式导入。如果类本身不可访问,那么访问类的公共成员就没有意义。

Q1-11.

给定以下代码,选择正确的选项:

package com.ejavaguru.courses;
class Course {
    public String courseName;
    public void setCourseName(private String name) {
        courseName = name;
    }
}
  1. 你不能将方法参数定义为private变量。
  2. 方法参数应该使用public或默认可访问性定义。
  3. 对于重写的方法,方法参数应该使用protected可访问性定义。
  4. 以上皆非。

答案:a

解释:你不能给方法参数添加显式的可访问性关键字。如果你这样做,代码将无法编译。

第二章. 使用 Java 数据类型

本章涵盖的考试目标 你需要了解的内容
[2.2] 区分对象引用变量和原始变量。 Java 中的原始数据类型,包括何时应该或可以不使用特定原始数据类型的场景。原始数据类型之间的相似之处和不同之处。原始数据类型和对象引用变量之间的相似之处和不同之处。
[2.1] 声明和初始化变量(包括原始数据类型的转换)。 原始数据类型和对象引用变量的声明和初始化。原始数据类型和对象引用变量的字面量值。
[2.5] 开发使用包装类(如 Boolean、Double 和 Integer)的代码。 当与包装类一起使用时,值是如何装箱和拆箱的以及何时进行装箱和拆箱。
[3.1] 使用 Java 运算符;包括括号来覆盖运算符优先级。 使用赋值、算术、关系和逻辑运算符与原始数据类型和对象引用变量。运算符的有效操作数。算术表达式的输出。确定两个原始数据类型的相等性。如何通过使用括号来覆盖默认的运算符优先级。

想象一下你刚刚购买了一套新房。你可能需要购买不同大小的容器来存储不同类型的食品,因为一个尺寸无法满足所有需求。此外,你也可能在家中移动食品——可能是因为随着时间的推移需求发生了变化(你希望食用它或你希望储存它)。

你的新厨房可以类比于 Java 如何使用不同的数据类型存储数据,并使用运算符来操作数据。食品项目就像 Java 中的数据类型,而用来存储食品的容器就像 Java 中的变量。触发食品项目状态变化的条件变化可以与处理逻辑相提并论。改变食品项目状态的变化因素(如火、热或冷却)可以与 Java 运算符相提并论。你需要这些变化因素,以便你可以处理原始食品项目以制作佳肴。

在 OCA Java SE 8 程序员 I 考试中,你将被问及 Java 中的各种数据类型,例如如何创建和初始化它们以及它们的相似之处和不同之处。考试还将询问你关于使用 Java 运算符的问题。本章涵盖了以下内容:

  • Java 中的原始数据类型

  • 原始 Java 数据类型的字面量值

  • Java 中的对象引用变量

  • 有效的和无效的标识符

  • Java 运算符的使用

  • 通过括号修改默认运算符优先级

2.1. 原始变量

[2.1] 声明和初始化变量(包括原始数据类型的转换)

[2.2] 区分对象引用变量和原始变量

在本节中,您将学习 Java 中的所有原始数据类型、它们的字面值以及创建和初始化原始变量的过程。定义为原始数据类型之一的变量是原始变量

如其名称所示,原始数据类型是编程语言中最简单的数据类型。在 Java 语言中,它们是预定义的。原始数据类型的名称相当描述了它们可以存储的值。Java 定义了以下八个原始数据类型:

  • char

  • byte

  • short

  • int

  • long

  • float

  • double

  • boolean

查看图 2.1(#ch02fig01)并尝试将给定的值与相应的类型匹配。

图 2.1. 将值与其对应类型匹配

这应该是一个简单的练习。表 2.1 提供了答案。

表 2.1. 将值与其对应的数据类型匹配
字符值 整数值 小数值 布尔型
a 100 7.3 true
4573

在前面的练习中,我将您需要存储的数据分类如下:字符型、整型、小数型和布尔型值。这种分类将在您面对选择最合适的原始数据类型来存储值时使您的生活更加简单。例如,要存储整数值,您需要一个能够存储整数值的原始数据类型;要存储小数,您需要一个可以存储小数的原始数据类型。简单,不是吗?

让我们映射原始数据类型可以存储的数据类型,因为这总是很容易分组和记住信息。

注意

布尔型类别与原始数据类型boolean或包装类Boolean不同。Java 的原始数据类型和类名使用代码字体显示。

原始数据类型可以分为以下类别:布尔型、字符型和数值型(进一步分为整型和浮点型)类型。请查看图 2.2 中的分类。

图 2.2. 原始数据类型的分类

如图 2.2 所示,char原始数据类型是一个无符号数值数据类型。它只能存储正整数。其余的数值数据类型(byteshortintlongfloatdouble)是有符号数值数据类型(它们可以存储正负值)。图 2.2 中的分类将帮助您进一步将每个数据类型与其可以存储的值关联起来。让我们从布尔类别开始。

2.1.1. 类别:布尔型

布尔型类别只有一个数据类型:boolean。一个boolean变量可以存储两个值之一:truefalse。它在只有两种状态可以存在的情况下使用。参见表 2.2 以获取问题及其可能的答案。

表 2.2. 可以使用boolean数据类型存储的合适数据
问题 可能的答案
你是否购买了考试券? 是/否
你今天登录过你的电子邮件账户吗? 是/否
你今天是否发推文谈论你的热情? 是/否
2001-2002 财年征收的税款 好问题!但无法以是/否的形式回答。
考试技巧

在这次考试中,问题测试你选择最适合的数据类型的能力,该条件只能有两种状态:是/否或真/假。这里的正确答案是boolean类型。

这里有一些定义boolean原始变量的代码:

boolean voucherPurchased = true;
boolean examPrepStarted = false;

在某些语言中,例如 JavaScript,在使用变量之前不需要定义其类型。在 JavaScript 中,编译器根据你分配给变量的值来定义变量的类型。相比之下,Java 是一种强类型语言。你必须声明一个变量并定义其类型,然后才能为其赋值。图 2.3 说明了定义一个boolean变量并为其赋值的过程。

图 2.3. 定义和赋值原始变量

这里要注意的另一点是分配给boolean变量的值。我使用了字面值truefalse来初始化boolean变量。字面值是一个固定值,不需要进一步计算就可以分配给任何变量。truefalse是唯一的两个boolean字面值。

注意

boolean类型只有两个字面值:truefalse

2.1.2. 类别:有符号数值

数值类别定义了两个子类别:整数和浮点数(也称为小数)。让我们从整数开始。

整数类型:byte, int, short, long

当你可以用整数计数一个值时,结果是整数。它包括负数和正数。表 2.3 列出了数据可以存储为整数的一些可能场景。

表 2.3. 可以归类为数值(非小数)数据类型的数据
情况 是否可以存储为整数?
Facebook 上的朋友数量
今天发布的推文数量
今天上传用于打印的照片数量
你的体温 不总是

你可以使用byteshortintlong数据类型来存储整数值。等等:为什么你需要这么多类型来存储整数?

每一个都可以存储不同范围的值。较小的类型的好处是明显的:它们在内存中需要的空间更少,并且处理速度更快。表 2.4 列出了所有这些数据类型,以及它们的尺寸和它们可以存储的值的范围。

表 2.4. 有符号数值 Java 原始数据类型存储的值范围
数据类型 大小 值范围
byte 8 位 –128 到 127,包括
short 16 位 –32,768 到 32,767,包括
int 32 位 –2,147,483,648 到 2,147,483,647,包括
long 64 位 –9,223,372,036,854,775,808 到 9,223,372,036,854,775,807,包括

OCA Java SE 8 程序员 I 考试可能会问你关于可以分配给 byte 数据类型的整数范围的问题,但不会包括关于可以存储在 shortintlong 数据类型中的整数值范围的问题。不用担心——你不需要记住所有这些数据类型的范围!

这里有一些代码示例,将字面值分配给原始数值变量,这些变量在其可接受范围内:

byte num = 100;
short sum = 1240;
int total = 48764;
long population = 214748368;

非十进制数字的默认类型是 int。要将整数字面值指定为 long 值,请添加后缀 Ll(小写的 L),如下所示:

long fishInSea = 764398609800L;

整数字面值有四种类型:二进制、十进制、八进制和十六进制:

  • 二进制数制— 一个基数为 2 的系统,它只使用两个数字,0 和 1。

  • 八进制数制— 一个基数为 8 的系统,它使用数字 0 到 7(总共 8 个数字)。在这里,十进制数字 8 表示为八进制 10,十进制数字 9 表示为 11,依此类推。

  • 十进制数制— 你每天使用的基数为 10 的数制。它基于 10 个数字,从 0 到 9(总共 10 个数字)。

  • 十六进制数制— 一个基数为 16 的系统,它使用数字 0 到 9 和字母 A 到 F(总共 16 个数字和字母)。在这里,数字 10 表示为 A 或 a,11 表示为 B 或 b,12 表示为 C 或 c,13 表示为 D 或 d,14 表示为 E 或 e,15 表示为 F 或 f。

让我们快速看一下如何将十进制数制中的整数转换为其他数制。图 2.4、2.5 和 2.6 展示了如何将十进制数 267 转换为八进制、十六进制和二进制数制。

图 2.4. 将十进制整数转换为八进制

图片

图 2.5. 将十进制整数转换为十六进制

图片

图 2.6. 将十进制整数转换为二进制

图片

考试技巧

在考试中,你不会要求将数字从十进制数制转换为八进制和十六进制数制,反之亦然。但你可以期待一些问题,这些问题要求你选择有效的整数字面值。图 2.4–2.6 将帮助你更好地理解这些数制,并更长时间地保留这些信息,这反过来又使你能够在考试中正确回答问题。

您可以使用十进制、二进制、八进制和十六进制来分配整数字面值。对于八进制字面值,使用前缀 0;对于二进制,使用前缀 0B0b;对于十六进制,使用前缀 0X0x。以下是一个每个前缀的示例:

图片

Java 7 引入了将下划线用作字面值一部分的使用。将字面值的单个数字或字母分组可以使它们更易于阅读。下划线对值没有影响。以下是一个有效的代码示例:

图片

要记住的规则

这里是一份关于在数值字面值中使用下划线的规则快速列表:

  • 你可以在前缀 0 后面直接放置一个下划线,用于定义八进制字面值。

  • 你不能以下划线开始或结束一个字面值。

  • 你不能在用于定义二进制和十六进制字面值的前缀 0b0B0x0X 后面直接放置下划线。

  • 你不能在 L 后缀(用于标记字面值为 long)之前放置下划线。

  • 你不能在期望一串数字的位置使用下划线(见以下示例)。

因为你在考试中可能会被问到字面值中下划线的有效和无效使用,让我们看看一些无效的例子:

图片 099fig02

以下代码行将成功编译但在运行时失败:

图片 099fig03_alt

因为 String 值可以接受下划线,编译器会编译之前的代码。但运行时会抛出一个异常,指出传递给 parseInt 方法的值格式无效。

这里是本章的第一个故事转折练习,供你尝试。它使用了数值字面值中多个下划线的组合。看看你是否能全部答对(答案在附录中)。

故事转折 2.1

让我们使用本节中定义的原始变量 baseDecimaloctValhexValbinVal,并引入额外的代码来打印所有这些变量的值。确定以下代码的输出:

class TwistInTaleNumberSystems {
public static void main (String args[]) {
        int baseDecimal = 267;
        int octVal = 0413;
        int hexVal = 0x10B;
        int binVal = 0b100001011;
        System.out.println (baseDecimal + octVal);
        System.out.println (hexVal + binVal);
    }
}

这里有一个快速练习——让我们定义和初始化一些使用下划线作为它们赋值中字面值的前缀的 long 原始变量。确定以下哪个正确地完成了这项工作:

long var1 = 0_100_267_760;
long var2 = 0_x_4_13;
long var3 = 0b_x10_BA_75;
long var4 = 0b_10000_10_11;
long var5 = 0xa10_AG_75;
long var6 = 0x1_0000_10;
long var7 = 100__12_12;
浮点数:float 和 double

当你期望十进制数时,你需要使用浮点数。例如,你能将事件发生的概率定义为整数吗?表 2.5 列出了可能场景,其中对应的数据存储为浮点数。

表 2.5. 存储为浮点数的数值
情况 答案是一个浮点数吗?
航天器的轨道力学 是(需要非常精确的值)
你朋友请求被接受的概率 是;概率介于 0.0(无)和 1.0(确定)之间
地球围绕太阳旋转的速度
里氏震级地震的震级

在 Java 中,你可以使用 floatdouble 原始数据类型来存储十进制数。float 比较节省空间,但可以存储的值范围比 double 小。float 的精度低于 double。即使数值在范围内,float 也可能无法准确表示某些数值。同样的限制也适用于 double——即使它是一个提供更多精度的数据类型。表 2.6 列出了 floatdouble 的值大小和范围。

表 2.6. 十进制数字的值范围
数据类型 大小 值范围
float 32 位 +/–1.4E–45 到+/–3.4028235E+38,+/–infinity,+/–0,NaN
double 64 位 +/–4.9E–324 到+/–1.7976931348623157E+308,+/–infinity,+/–0,NaN

下面是一些实际运行的代码示例:

float average = 20.129F;
float orbit = 1765.65f;
double inclination = 120.1762;

你注意到在前面代码中初始化变量averageorbit时使用了后缀Ff吗?十进制字面量的默认类型是double,但通过在十进制字面量值后附加Ff后缀,你告诉编译器该字面量值应被当作float处理,而不是double

你也可以按照以下方式将字面量十进制值分配为科学记数法:

图片

你也可以将后缀Dd添加到十进制数值中,以指定它是double值。因为十进制数的默认类型是double,所以使用后缀Dd是多余的。检查以下代码行:

图片

从 Java 7 版本开始,你也可以在浮点字面量值中使用下划线。规则通常与之前提到的数值字面量规则相同;以下规则是针对浮点字面量特定的:

  • 你不能在下划线之前放置DdFf后缀(这些后缀用于标记浮点字面量为doublefloat)。

  • 不能在十进制点旁边放置下划线。

让我们看看一些示例,这些示例展示了在浮点字面量值中无效使用下划线的情况:

图片

2.1.3. 类别:字符(无符号整数)

字符类别定义了唯一的数据类型:charchar是一个无符号整数。它可以存储单个 16 位 Unicode 字符;也就是说,它可以存储几乎所有现有脚本和语言中的字符,包括日语、韩语、中文、德文、法语、德语和西班牙语。因为你的键盘可能没有键来表示所有这些字符,你可以使用\u0000(或0)的值到最大值\uffff(或65,535)的值。以下代码显示了将值分配给char变量的示例:

图片

一个非常常见的错误是使用双引号将值分配给char。正确的选项是单引号。图 2.7 显示了两个(假设的)程序员保罗和哈里的对话。

图 2.7. 永远不要使用双引号来分配一个字母作为char值。

图片

如果尝试使用双引号分配一个char,代码将无法编译,并显示以下信息:

Type mismatch: cannot convert from String to char
考试技巧

永远不要使用双引号将字母分配给char变量。双引号用于将值分配给类型为String的变量。

在内部,Java 将char数据存储为无符号整数值(正整数)。因此,将正整数值分配给char是可以接受的,如下所示:

图片

注意

考试将测试你对多种(隐晦的)技术,例如将无符号整数值分配给 char 数据类型。但我不建议在实际项目中使用这些技术。请编写可读性和易于维护的代码。

整数值 122 等价于字母 z,但整数值 122 不等于 Unicode 值 \u0122。前者是十进制数(使用数字 0-9),后者是十六进制数(使用数字 0-9 和字母 a-f——大小写均可)。\u 用于标记值作为 Unicode 值。你必须使用引号将 Unicode 值分配给 char 变量。以下是一个示例:

char c2 = '\u0122';
System.out.println("c1 = " + c1);
System.out.println("c2 = " + c2);

图 2.8 展示了在支持 Unicode 字符的系统上运行前面代码的输出。

图 2.8. 使用整数值 122 与 Unicode 值 \u0122 分配字符的输出

如前所述,char 值是无符号整数值,因此如果你尝试将负数分配给一个 char,代码将无法编译。以下是一个示例:

但你可以通过强制转换为 char 类型来给 char 类型分配一个负数,如下所示:

在前面的代码中,注意字面值 –122 前面有 (char) 前缀。这种做法称为 类型转换。类型转换是将一种数据类型强制转换为另一种数据类型。

你只能转换兼容的数据类型。例如,你可以将 char 转换为 int,反之亦然。但你不能将 int 转换为 boolean 值或反之亦然。当你将更大的值转换为范围较小的数据类型时,你告诉编译器你知道自己在做什么,因此编译器会通过截断任何可能不适合较小变量的额外位来继续操作。谨慎使用类型转换——它可能不会总是给出正确的转换值。

图 2.9 展示了将值转换为 c3(值看起来很奇怪!)的前面代码的输出。

图 2.9. 分配字符变量负值的输出

Java 中的 char 数据类型不分配空间来存储整数的符号。如果你尝试强制将负整数分配给 char,符号位将存储为整数值的一部分,这会导致存储意外的值。

考试技巧

考试将测试你对 char 类型变量可能分配的值的理解,包括分配是否会导致编译错误。不用担心——它不会测试你在将任意整数值分配给 char 后实际显示的值!

2.1.4. 对原始数据类型名称的混淆

如果你之前在其他编程语言中工作过,你可能会对 Java 和其他语言中的基本数据类型名称感到困惑。例如,C 定义了一个原始的 short int 数据类型。但在 Java 中,shortint 是两个不同的原始数据类型。OCA Java SE 8 程序员 I 考试将测试你识别原始数据类型名称的能力,这些问题答案可能不会立即明显。以下是一个例子:

问题:以下代码的输出是什么?

public class MyChar {
    public static void main(String[] args) {
        int myInt = 7;
        bool result = true;
        if (result == true)
            do
                System.out.println(myInt);
            while (myInt > 10);
    }
}
  1. 它打印 7 一次。

  2. 它什么也不打印。

  3. 编译错误。

  4. 运行时错误。

正确答案是 (c)。这个问题试图通过不使用任何 if 构造或 do-while 循环的复杂代码来欺骗你!正如你所看到的,它使用了一个不正确的数据类型名称 bool 来声明和初始化变量 result。因此,代码将无法编译。

考试技巧

注意那些使用不正确的基本数据类型名称的问题。例如,Java 中没有 bool 基本数据类型。正确的数据类型是 boolean。如果你在其他编程语言中工作过,你可能会在尝试记住 Java 中使用的所有基本数据类型的确切名称时感到困惑。记住,只有两种基本数据类型——intchar——被缩短;其余的基本数据类型(byteshortlongfloatdouble)没有被缩短。

2.2. 标识符

标识符是包、类、接口、方法和变量的名称。虽然识别有效的标识符并没有明确包含在考试目标中,但你可能会遇到类似以下的问题,这些问题将要求你识别有效的和无效的标识符:

问题:以下哪一行代码可以成功编译?

  1. byte exam_total = 7;

  2. int exam-Total = 1090;

正确答案是 (a)。选项 (b) 是错误的,因为在 Java 标识符的名称中不允许使用连字符。下划线是允许的。

2.2.1. 有效的和无效的标识符

表 2.7 包含了一组规则,这些规则将帮助你正确地定义有效的(以及无效的)标识符,以及一些示例。

表 2.7. 有效的和无效标识符的成分
有效的标识符的特性 无效的标识符的特性
无限制长度 与 Java 保留字或关键字拼写相同(见 表 2.8)
以字母(a–z,大写或小写)、货币符号或下划线开头 使用特殊字符:!、@、#、%、^、&、*、(、)、'、:、;、、/、\、}
可以使用数字(但不能位于起始位置) 以 Java 数字(0–9)开头
可以使用下划线(在任何位置)
可以使用货币符号(在任何位置):¥、$、£、¢、¥ 和其他
有效的标识符的例子 无效的标识符的例子
customerValueObject 7world (标识符不能以数字开头)
$rate, £Value, _sine %value (标识符不能使用特殊字符%)
happy2Help, nullValue Digital!, books@manning (标识符不能使用特殊字符!或@)
常量 null, true, false, goto (标识符不能与 Java 关键字或保留词同名)

你不能定义与 Java 关键字或保留词相同的变量名。正如这些名称所暗示的,它们是为特定目的保留的。[表 2.8 列出了不能用作 Java 变量名的 Java 关键字、保留词和字面量。

表 2.8. 不能用作 Java 变量名的 Java 关键字和保留词
abstract default goto package this
assert do if private throw
boolean double implements protected throws
break else import public transient
byte enum instanceof return true
case extends int short try
catch false interface static void
char final long strictfp volatile
class finally native super while
const float new switch
continue for null synchronized

让我们通过以下变量声明来对抗确定正确和错误变量时的一些常见错误:

接下来,让我们看看对象引用变量以及它们与原始变量的区别。

2.3. 对象引用变量

[2.1] 声明和初始化变量(包括原始数据类型的转换)

[2.2] 区分对象引用变量和原始变量

Java 中的变量可以分为两种类型:原始变量引用变量。在本节中,除了对引用变量进行简要介绍外,我们还将介绍引用变量和原始变量之间的基本区别。

引用变量也被称为对象引用变量对象引用。我在本文中使用这些术语是通用的。

2.3.1. 什么是对象引用变量?

对象是类的实例,包括预定义和用户定义的类。对于 Java 中的引用类型,变量名计算出的值是存储在内存中变量引用的对象的地址。实际上,对象引用是一个内存地址,它指向存储对象数据的内存区域。

让我们快速定义一个基本的类,Person,如下所示:

class Person {}

当使用new运算符实例化对象时,会返回该对象的内存地址值。这个地址通常被分配给引用变量。图 2.10 显示了创建类型为Person的引用变量person并将其赋值的代码行。

图 2.10. 引用变量的创建和赋值

当执行图 2.10 所示的语句时,会发生三件事:

  • 创建了一个新的Person对象。

  • 在栈中创建了一个名为person的变量,其值为空(null)。

  • 变量person被分配到对象所在内存地址的值。

图 2.11 包含了一个引用变量及其在内存中所引用的对象的插图。

图 2.11. 内存中一个引用变量及其所引用的对象

您可以将对象引用变量视为访问对象属性的把手。以下类比将帮助您理解对象引用变量、它们所引用的对象以及它们之间的关系。将对象类比为,将对象引用类比为皮带。尽管这个类比可能不会深入分析,以下比较是有效的:

  • 未系在狗上的皮带是一个具有null值的引用对象变量。

  • 没有皮带的狗是一个没有任何对象引用变量引用的 Java 对象。

  • 正如一只未拴绳的狗可能会被动物控制部门带走一样,没有被引用变量引用的对象可能会被垃圾回收(由 JVM 从内存中移除)。

  • 几条皮带可能系在一只狗上。同样,Java 对象可能被多个对象引用变量引用。

图 2.12 展示了这个类比。

图 2.12. 理解对象的狗皮带类比

所有类型的对象引用变量的默认值都是null。您也可以显式地将null值赋给引用变量。以下是一个示例:

Person person = null;

在这种情况下,引用变量person可以比作没有狗的皮带。

注意

所有类型的对象引用变量的字面值都是null

2.3.2. 区分对象引用变量和原始变量

正如约翰·格雷(John Gray,《火星人来自火星,金星人来自金星》一书的作者)所说,男人和女人在本质上是有区别的,原始变量和对象引用变量在多个方面也有区别。基本区别是原始变量存储实际值,而引用变量存储它们所引用的对象的地址。

假设已经定义了一个Person类。如果您创建一个int变量a和一个对象引用变量person,它们将在内存中存储它们的值,如图 2.13 所示。

int a = 77;
Person person = new Person();
图 2.13. 原始变量存储实际值,而对象引用变量存储它们所引用的对象的地址。

原始变量和对象引用变量之间的重要差异如图 2.14 所示,即一个女孩和一个男孩之间的对话。女孩代表对象引用变量,男孩代表原始变量。(如果你不理解所有这些类比,不要担心。在阅读后续章节的相关主题后,它们会更有意义。)

在下一节中,你将开始使用运算符操作这些变量。

图 2.14. 对象引用变量和原始变量之间的差异

图片

2.4. 运算符

[3.1] 使用 Java 运算符;包括括号以覆盖运算符优先级

在本节中,你将使用不同类型的运算符——赋值、算术、关系和逻辑——来操作变量的值。你将编写代码来确定两个原语数据类型的相等性。你还将学习如何通过使用括号来修改运算符的默认优先级。对于 OCA Java SE 8 程序员 I 级考试,你应该能够处理表 2.9 中列出的运算符。

表 2.9. 运算符类型和相关运算符
运算符类型 运算符 目的
赋值 =, +=, -=, *=, /= 将值赋给变量
算术 +, -, *, /, %, ++, -- 加、减、乘、除和取模原语
关系 <, <=, >, >=, ==, != 比较原语
逻辑 !, &&, || 对原语应用 NOT、AND 和 OR 逻辑
注意

并非所有运算符都可以与所有类型的操作数一起使用。例如,你可以确定一个数字是否大于另一个数字,但你不能确定 true 是否大于 false 或一个数字是否大于 true。在学习本考试中所有运算符的使用时,请注意这一点。

2.4.1. 赋值运算符

你需要为考试准备的赋值运算符是 =, +=, -=, *=, 和 /=.

简单赋值运算符 = 是最常用的运算符。它用于初始化变量并重新分配新值给它们。

+=, -=, *=, 和 /= 运算符是带有赋值的加法、减法、乘法和除法的简写形式。+= 运算符可以读作“先加后赋”,-= 可以读作“先减后赋”。同样,*= 可以读作“先乘后赋”,/= 可以读作“先除后赋”,%= 可以读作“先取模后赋”。如果你将这些运算符应用于两个操作数 ab,它们可以表示如下:

a -= b is equal to a = a – b
a += b is equal to a = a + b
a *= b is equal to a = a * b
a /= b is equal to a = a / b
a %= b is equal to a = a % b

让我们看看一些有效的代码行:

图片

接下来让我们看看一些无效的代码行:

图片

现在我们尝试将可以存储更大范围值的变量挤入范围较短的变量中。尝试以下赋值:

它与图 2.15 中显示的内容类似,其中有人试图强行将更大的值(long)挤入较小的容器(int)中。

图 2.15. 将更大的值(long)分配给只能存储较小值范围的变量(int

您可以通过显式地将更大的值转换为更小的值,将更大的值分配给只能存储较小范围的变量。这样做,您告诉编译器您知道自己在做什么。在这种情况下,编译器会通过截断任何可能不适合较小变量的额外位来继续操作。小心!虽然截断额外位可以使更大的值适合较小的数据类型,但剩余的位不会表示原始值,并可能产生意外的结果。

将上一个赋值示例(将 long 分配给 int)与以下示例进行比较,该示例将较小的值(int)分配给可以存储较大值范围的变量(long):

int 可以轻松地放入 long 中,因为足够的空间(如图 2.16 所示)。

图 2.16. 将较小的值(int)分配给可以存储较大值范围的变量(long

考试技巧

您不能使用赋值运算符将 boolean 值分配给类型为 charbyteintshortlongfloatdouble 的变量,反之亦然。

您也可以使用赋值运算符在同一行上分配多个值。请检查以下代码行:

在标记为 的行上,赋值是从右到左开始的。变量 c 的值被分配给变量 b,而变量 b(它已经等于 c)的值被分配给变量 a。这可以通过第 3 行打印 8,而不是 7 来证明!

故事转折 2.15 中的下一个故事在变量分配和初始化方面加入了一些转折。让我们看看您是否能识别出错误的地方(答案见附录)。

故事转折 2.2

让我们修改之前章节中使用的 boolean 变量的赋值和初始化。检查以下代码初始化并选择错误的答案:

public class Foo {
    public static void main (String args[]) {
        boolean b1, b2, b3, b4, b5, b6;    // line 1
        b1 = b2 = b3 = true;               // line 2
        b4 = 0;                            // line 3
        b5 = 'false';                      // line 4
        b6 = yes;                          // line 5
    }
}
  1. 第 1 行的代码将无法编译。

  2. 不能像第 2 行代码那样初始化多个变量。

  3. 第 3 行的代码是正确的。

  4. 不能将 'false' 分配给 boolean 变量。

  5. 第 5 行的代码是正确的。

2.4.2. 算术运算符

让我们快速查看每个运算符,以及一个简单的示例,表 2.10 中有介绍。

表 2.10. 使用算术运算符的示例
运算符 目的 用法 答案
+ 加法 12 + 10 22
- 减法 19 – 29 -10
* 乘法 101 * 45 4545
/ 除法(商) 10 / 6 10.0 / 6.0 1 1.6666666666666667
% 取模(除法的余数) 10 % 6 10.0 % 6.0 4 4.0
++ 一元自增运算符;值增加 1 ++var 或 var++ 11(假设 var 的值为 10)
-- 一元自减运算符;值减少 1 --var 或 var-- 9(假设 var 的值为 10)
考试技巧

你可以使用一元自增和自减运算符与变量一起使用,但不能与字面量一起使用。如果你这样做,代码将无法编译。

当你将加法运算符应用于 char 类型的值时,它们的对应 ASCII 值会被相加和相减。以下是一个快速示例(字符 a 的 ASCII 值为 97):

图片

以下代码输出 0:

图片

考试技巧

你可以使用所有算术运算符与 char 原始数据类型一起使用,包括一元自增和自减运算符。

算术运算中的数据类型隐式提升

所有 byteshortchar 类型的值在用作算术运算的操作数时都会自动提升为 int 类型。如果涉及到 long 类型的值,那么包括 int 值在内的所有值都会提升为 long 类型。这解释了为什么你不能将两个 byte 类型的值的和赋值给 short 类型的变量:

图片

上述代码会因以下错误信息而失败:

incompatible types: possible lossy conversion from int to short
short sum = age1 + age2;
                 ^
1 error
考试技巧

对于 charbyteshortint 数据类型的算术运算,所有操作数值都会提升为 int。如果算术运算包括 long 数据类型,所有操作数值都会提升为 long。如果算术运算包括 floatdouble 数据类型,所有操作数值都会提升为 double

但如果你修改上述示例,并将变量 age1age2 定义为 final 变量,那么编译器 可以保证 它们的和,值 30,可以被赋值给 short 类型的变量,而不会丢失精度。在这种情况下,编译器会将 age1age2 的和赋值给 sum。以下是修改后的代码:

图片

++ 和 --(一元自增和自减运算符)

运算符 ++-- 是一元运算符;它们与单个操作数一起工作。它们用于将变量的值增加或减少 1

一元运算符也可以使用前缀和后缀表示法。在 前缀表示法 中,运算符出现在其操作数之前:

图片

后缀表示法 中,运算符出现在其操作数之后:

图片

当这些运算符不是表达式的一部分时,后缀和前缀表示法的行为完全相同:

图片

当一元运算符在表达式中使用时,其相对于操作数的放置决定了其值是在表达式评估之前还是之后增加或减少。请看以下代码,其中运算符 ++ 使用了前缀表示法:

图片

在前面的例子中,表达式 a - ++b 使用了前缀表示法中的增量运算符 (++)。因此,变量 b 的值增加到 11,然后从 20 中减去,将结果 9 赋值给变量 c

当使用后缀表示法与操作数一起使用++时,它的值在使用表达式后增加:

这里有趣的部分是,在两种情况下 b 的值都打印为 11,因为变量 b 的值在它所使用的表达式评估后立即增加(或减少)。

同样的逻辑也适用于单目运算符 --。以下是一个示例:

让我们使用后缀递减运算符 (--) 并看看会发生什么:

让我们检查一些示例代码,这些代码在同一行代码中使用前缀和后缀表示法的单目增量运算符和递减运算符。你认为以下代码的输出会是什么?

int a = 10;
a = a++ + a + a-- - a-- + ++a;
System.out.println(a);

此代码的输出是32。右侧表达式的评估是从左到右进行的,以下值评估为32

a = 10 + 11 + 11 - 10 + 10;

表达式的评估从左到右开始。对于前缀单目运算符,其操作数的值在使用表达式之前增加或减少。对于后缀单目运算符,其操作数的值在使用表达式之后增加或减少。图 2.17 说明了前面表达式中发生的情况。

图 2.17. 具有多处后缀和前缀单目运算符出现的表达式评估

对于考试,您需要很好地理解并练习使用后缀和前缀运算符。除了前面示例中显示的表达式外,您还可以在 if 语句、for 循环以及 do-whilewhile 循环的条件中找到它们的使用。

下一个故事转折练习将为您提供在前缀和后缀表示法中使用的单目运算符的实践(答案见附录)。

故事转折 2.3

让我们修改 图 2.17 中使用的表达式,将所有前缀表示法中的单目运算符替换为后缀表示法,反之亦然。因此 ++a 变为 a++,反之亦然。同样,--a 变为 a--,反之亦然。您的任务是评估修改后的表达式并确定以下代码的输出:

int a = 10;
a = ++a + a + --a - --a + a++;
System.out.println (a);

尝试通过替换表达式中变量 a 的值来形成表达式,并解释每个值,就像在 图 2.17 中为您所做的那样。

2.4.3. 关系运算符

关系运算符用于检查一个条件。您可以使用这些运算符来确定一个原始值是否等于另一个值,或者它是否小于或大于另一个值。

这些关系运算符可以分为两类:

  • 比较大于(>, >=)和小于(<, <=)的值

  • 比较值(==)和不等(!=

<, <=, >, 和 >= 运算符适用于所有类型的数字,包括整数(包括char)和浮点数,可以进行加法和减法运算。查看以下代码:

第二类运算符将在下一节中介绍。

考试技巧

你不能比较不可比较的值。例如,你不能比较一个boolean与一个int、一个char或一个浮点数。如果你尝试这样做,你的代码将无法编译。

比较原始数据类型(使用 == 和 !=)

==(等于)和!=(不等于)运算符可以用来比较所有类型的原始数据类型:charbyteshortintlongfloatdoubleboolean。如果比较的原始数据类型值相等,==运算符返回booleantrue,否则返回false。如果比较的原始数据类型值不相等,!=运算符返回true,否则返回false。对于同一组值,如果==返回true,则!=将返回false。听起来很有趣!

查看以下代码:

记住,你不能将这些运算符应用于不可比较的类型。在下面的代码片段中,比较一个int变量和一个boolean变量的代码将无法编译:

这里是编译错误:

incomparable types: int and boolean
System.out.println(a == b1);
                      ^
考试技巧

关系运算的结果始终是一个boolean值。你不能将关系运算的结果赋值给类型为charintbyteshortlongfloatdouble的变量。

使用赋值运算符(=)比较原始数据类型

使用赋值运算符=代替相等运算符==来比较原始数据类型是一个非常常见的错误。在继续阅读之前,请查看以下代码:

在前面的例子中,并不是在比较变量ab。它将变量b的值赋给变量a,然后打印变量a的值,该值为20。同样,并不是在比较变量b1与布尔字面量true。它将布尔字面量true赋给变量b1,然后打印变量b1的值。

注意

你不能使用赋值运算符=来比较原始数据类型。

2.4.4. 逻辑运算符

逻辑运算符用于评估一个或多个表达式。这些表达式应该返回一个boolean值。你可以使用逻辑运算符ANDORNOT来检查多个条件并相应地执行。以下是一些现实生活中的例子:

  • 案例 1(针对管理者)—— 如果客户对交付的项目非常满意,并且你认为你应坐在老板的位置上,请要求晋升!

  • 案例 2(针对学生)— 如果薪资和福利优厚或工作前景出色,则接受工作提议。

  • 案例 3(针对初级 Java 程序员)— 如果对当前工作不满意,则更换工作。

在这些示例案例中,你只有在满足一系列条件的情况下才会做出决定(请求晋升、接受工作提议或更换工作)。在案例 1 中,如果满足所有指定条件,经理才可能请求晋升。在案例 2 中,如果任一条件为真,学生可以接受新的工作。在案例 3 中,如果初级 Java 程序员对当前工作不满意,他们可以更换工作,也就是说,如果指定的条件(对当前工作满意)为假。

如这些示例所示,如果你希望在两个条件都为真时执行任务,请使用逻辑AND运算符&&。如果你希望在任一条件为真时执行任务,请使用逻辑OR运算符||。如果你要反转boolean值的输出,请使用否定运算符!

现在看看代码的实际应用:

打印false,因为两个条件a > 20b > 10都不是true。第一个条件(a > 20)是false 打印true,因为其中之一的条件(b > 10)是true 打印false,因为指定的条件b > 10true 打印true,因为指定的条件a > 20false

表 2.11 将帮助你理解使用这些逻辑运算符的结果。

表 2.11. 使用逻辑运算符 ANDORNOTboolean 文字值的输出结果
运算符 && (AND) 运算符 || (OR) 运算符 ! (NOT)
true && true → true true && false → false false && true → false false && false → false true && true && false → false true

下面是这个表的总结:

  • 逻辑 AND (&&)—如果所有操作数都是true,则返回true;否则返回false

  • 逻辑 OR (||)—如果任一或所有操作数都是true,则返回true

  • 逻辑否定(!—否定boolean值。对于false返回true,反之亦然。

运算符 |& 也可以用来操作数值的各个位,但在这里不涉及这种用法,因为它不是本次考试的考点。

&& 和 || 是短路运算符

关于逻辑运算符 &&|| 的另一个有趣点是,它们也被称为短路运算符,因为它们通过评估操作数来确定结果的方式。让我们从运算符 && 开始。

&& 运算符仅在两个操作数都为 true 时返回 true。如果此运算符的第一个操作数评估为 false,则结果永远不能为 true。因此,&& 不会评估第二个操作数。同样,如果第一个操作数评估为 true,则 || 运算符不会评估第二个操作数。

在第一个打印语句 中,因为第一个条件 total < marks 评估为 false,所以下一个条件 ++marks > 5 甚至没有被评估。正如你所见 marks 的输出值仍然是 8(它在第 1 行初始化的值)!同样,在下一个比较 中,因为 total == 10 评估为 true,所以第二个条件 ++marks > 10 没有被评估。再次,这可以在再次打印 marks 的值时得到验证 ,输出为 8

注意

所有关系和逻辑运算符都返回一个 boolean 值,该值可以赋给原始 boolean 变量。

下一个故事转折的目的是鼓励你玩使用短路运算符的代码。为了确定作为短路运算符操作数的 boolean 表达式是否评估,你可以对表达式中所使用的变量应用一元增量运算符(后缀表示法)。比较新变量的值与旧值,以验证表达式是否已评估(答案见附录)。

故事转折 2.4

如你所知,短路运算符 &&|| 如果仅通过评估第一个操作数就可以确定表达式的结果,则可能不会评估它们的操作数。检查以下代码,并圈出你认为将评估的表达式。围绕你认为可能不会执行的表达式画一个方框。(例如,在第 1 行,a++ > 10++b < 30 都将评估。)

class TwistInTaleLLogicalOperators {
    public static void main (String args[]) {
        int a = 10;
        int b = 20;
        int c = 40;
        System.out.println(a++ > 10 || ++b < 30);     // line1
        System.out.println(a > 90 && ++b < 30);
        System.out.println(!(c>20) && a==10 );
        System.out.println(a >= 99 || a <= 33 && b == 10);
        System.out.println(a >= 99 && a <= 33 || b == 10);
    }
}

示例中在真实项目中使用短路运算符 && 的应用

逻辑运算符 && 常用于代码中,以检查在调用对象引用变量上的方法之前是否已为其分配了值:

String name = "hello";
if (name != null && name.length() > 0)
      System.out.println(name.toUpperCase());

2.4.5. 运算符优先级

如果你在一行代码中使用多个操作数和多个运算符会发生什么?哪一个应该被视为“国王”并给予优先权?

别担心。Java 已经为这种情况制定了规则。表 2.12 列出了运算符的优先级:上方的运算符具有最高的优先级,同一组内的运算符具有相同的优先级,并且从左到右进行评估。

表 2.12. 运算符优先级
运算符 优先级
后缀 表达式++,表达式--
一元 ++表达式,--表达式,+表达式,-表达式,!
乘法 * (乘),/ (除),% (余数)
加法 + (加),- (减)
关系 <, >, <=, >=
等于 ==, !=
逻辑与 &&
逻辑或 ||
赋值 =, +=, -=, *=, /=, %=
注意

表 2.12 仅限于 OCA 考试中的运算符。您可以在docs.oracle.com/javase/tutorial/java/nutsandbolts/operators.html访问完整列表。

让我们执行一个使用多个运算符(具有不同优先级)的表达式:

因为这个表达式 定义了多个具有不同优先级的运算符,所以它的评估如下:

(((int1 % int2) * int3)) + (int1 / int2)
(((10 % 20) * 30)) + (10 / 20)
( (10       * 30)) + (0)
( 300 )

如果您不想以这种方式评估表达式,补救措施很简单:使用括号来覆盖默认的运算符优先级。以下是一个在乘以 int2 之前先添加 int3int1 的示例:

注意

您可以使用括号来覆盖默认的运算符优先级。如果您的表达式定义了多个运算符,并且您不确定您的表达式将如何被评估,请使用括号以您首选的顺序进行评估。内部括号先于外部括号进行评估,遵循经典代数的相同规则。

2.5. 包装类

[2.5] 开发使用包装类(如 Boolean、Double 和 Integer)的代码。

Java 为它的每个原始数据类型定义了一个包装类。包装类用于将原始数据类型包装在对象中,因此它们可以被添加到集合对象中。它们使所有类型都可以像对象实例一样被对待。包装类帮助您编写更干净的代码,易于阅读。对于这次考试,您应该能够编写使用这些包装类的代码。

2.5.1. 包装类的类层次结构

所有包装类都是不可变的——这些类在初始化后不允许对其实例的状态进行更改。它们共享多个使用细节和方法。图 2.18 显示了它们的层次结构。

图 2.18. 包装类的层次结构

所有数值包装类都扩展了类 java.lang.Number。类 BooleanCharacter 直接扩展了类 Object。所有包装类都实现了接口 java.io.Serializablejava.lang.Comparable。所有这些类都可以被序列化到流中,并且它们的对象定义了一个自然排序顺序。

2.5.2. 创建包装类对象

您可以通过多种方式创建所有包装类的对象:

  • 赋值—— 通过将原始数据类型赋给包装类变量(自动装箱)

  • 构造函数—— 通过使用包装类构造函数

  • 静态方法—— 通过调用包装类的静态方法,例如,valueOf()

例如:

您可以用类似的方式创建其他包装类(ShortIntegerLongFloat)的对象。所有包装类都定义了构造函数,可以使用相应的原始值或作为 String 来创建对象。

另一个值得注意的有趣点是,这些类中没有一个定义默认的无参构造函数。包装类是不可变的。因此,如果它们以后不能被修改,用默认的原始值初始化包装对象是没有意义的。

考试技巧

所有包装类(除了 Character)都定义了一个接受表示需要包装的原始值的 String 参数的构造函数。请注意考试中包含调用包装类无参构造函数的问题。这些类中没有一个定义无参构造函数。

您可以直接将原始值赋给其包装类类型的引用变量——这要归功于 自动装箱。相反,当原始包装类的对象转换为相应的原始值时,这是 自动拆箱。我将在下一节详细讨论自动装箱和自动拆箱。

2.5.3. 从包装类获取原始值

所有包装类都定义了格式为 primitiveValue() 的方法,其中术语 primitive 指的是确切的原始数据类型名称。表 2.13 展示了这些类及其获取相应原始值的方法列表。

表 2.13. 从包装类获取原始值的方法
Boolean Character Byte, Short, Integer, Long, Float, Double
booleanValue() charValue() byteValue(), shortValue(), intValue(),
longValue(), floatValue(), doubleValue()

值得注意的是,所有数值包装类都定义了方法来检索它们存储的原始值的值,作为 byteshortintlongfloatdouble

考试技巧

所有的六个数值包装类都从它们的共同超类 Number 继承了所有六个 *****Value() 方法。

2.5.4. 将字符串值解析为原始类型

要获取与字符串值对应的原始数据类型值,您可以使用静态实用方法 parseDataType,其中 DataType 指的是返回值的类型。每个包装类(除了 Character)都定义了一个将 String 解析为相应原始值的方法,如 表 2.14 中所示。

表 2.14. 包装类中 parseDataType 方法的列表
类名 方法
Boolean public static boolean parseBoolean(String s)
字符 没有对应的解析方法
Byte public static byte parseByte(String s)
Short public static short parseShort (String s)
Integer public static int parseInt(String s)
Long public static long parseLong(String s)
Float public static float parseFloat(String s)
Double public static double parseDouble(String s)

所有这些解析方法对于无效值都会抛出 NumberFormatException。以下是一些示例:

图片

考试技巧

除了Boolean.parseBoolean()方法外,所有解析方法(列于表 2.14)都会抛出NumberFormatException。此方法返回false,当它解析的字符串不等于“true”(不区分大小写比较)时。

2.5.5. 使用 valueOf 方法和包装类构造函数的区别

valueOf()方法传入原始类型或String类型的参数时,它返回相应包装类的对象。那么,valueOf()方法和这些类的构造函数之间的区别是什么,这些构造函数也接受原始类型和String类型的参数?

包装类ByteShortIntegerLong缓存值在-128127范围内的对象。Character类缓存值为0127的对象。这些类定义了内部静态类,它们在数组中存储原始值-1281270127的对象。如果您请求这些类中的任何一个对象,在这个范围内,valueOf()方法返回一个指向预定义对象的引用;否则,它创建一个新的对象并返回其引用:

图片

考试技巧

包装类FloatDouble不会为任何值范围内的对象缓存。

Boolean类的例子中,缓存的实例可以直接访问,因为只有两个:静态常量Boolean.TRUEBoolean.FALSE

2.5.6. 比较包装类对象

您可以使用equals方法或比较运算符(即==)来比较包装类对象的相等性。equals方法始终比较包装实例存储的原始值,而==比较对象引用。如果被比较的变量引用相同的实例,则运算符==返回true

参考前面关于valueOf()的部分。包装类如CharacterByteShortIntegerLong为值0127-128127缓存包装对象。根据您如何初始化包装实例,它们可能或可能不引用相同的实例。以下示例使用构造函数、静态方法valueOf和自动装箱(将在下一节中介绍)初始化Integer变量。让我们使用==比较这些引用:

图片

System.out.println(i1 == i2);
System.out.println(i3 == i4);
System.out.println(i4 == i5);
System.out.println(i5 == i6);

以下是前面代码的输出:

false
true
true
true

从前面代码的输出可以看出,使用valueOf方法和自动装箱为int10创建的Integer实例引用了相同的实例。如果在前面代码的这些行中将==替换为equals(),它们将输出true

图片

但对于使用==比较的int200创建的Integer实例并不适用(因为它们没有存储在Integer缓存中):

图片

再次,如果在前面代码中将==替换为equals(),所有比较的输出将为true

考试技巧

包装类Boolean存在缓存实例,用于值truefalseCharacter类缓存从0127的值的实例。ByteShortIntegerLong类为值-127128缓存实例。FloatDouble包装类没有缓存实例。

equals方法比较包装实例存储的值。比较运算符==比较引用变量——检查它们是否指向同一个实例。

使用 hashCode()和 equals()确定包装类实例的相等性

包装类实例可以用作 Java 集合框架中的键,与支持键值对的类(如HashMap)一起使用。这些类使用hashCode()equals()来确定实例的相等性。由于集合框架类(除了ArrayList)不在此考试范围内,因此我在本书中没有涉及它们。

如果包装实例不是同一类,则不能使用equals()==来比较它们的相等性。对于使用==比较的实例,代码将无法编译。当使用equals()比较时,输出将是false

![130fig01_alt.jpg]

考试技巧

值相同的不同包装类对象不相等。使用equals()与这样的实例将返回false。如果你使用==与这样的实例,代码将无法编译。

下一节将介绍自动装箱和拆箱,编译器使用这些功能将原始值转换为包装对象,反之亦然。

2.5.7. 自动装箱和拆箱

自动装箱是将原始数据类型自动转换为相应包装类的对象(你装箱原始值)。拆箱是相反的过程(你拆箱原始值),如图 2.19 所示。

图 2.19. 自动装箱和拆箱

![02fig19_alt.jpg]

包装类频繁地使用自动装箱和拆箱功能:

![130fig02_alt.jpg]

将前面方法的使用与类Double定义的以下方法进行比较:

public int compareTo(Double anotherDouble)

等等——我刚刚提到类Double中定义的compareTo()方法接受一个Double类的对象,而不是一个double原始数据类型吗?那么为什么前面的代码可以编译呢?答案是自动装箱。Java 将原始的double转换为Double类的对象(通过使用valueOf()方法),所以它工作正常。Java 编译器在运行时将其转换为以下代码:

Double d1 = new Double(12.67D);
System.out.println(d1.compareTo(Double.valueOf(21.68D)));

现在检查以下代码(自动装箱的拆箱示例):

![131fig01_alt.jpg]

在前面的代码中,在for循环执行结束时,total将被分配一个值为23.36Double对象。算术运算符如+=不能与对象一起使用。那么你认为代码为什么可以编译呢?在这个例子中,Java 编译器在运行时将前面的代码转换为以下代码:

public class Unbox {
    public static void main(String args[]) {
        ArrayList list = new ArrayList();
        list.add(new Double(12.12D));
        list.add(new Double(11.24D));
        Double total = Double.valueOf(0.0D);
        for(Iterator iterator = list.iterator(); iterator.hasNext();) {
            Double d = (Double)iterator.next();
            total = total.doubleValue() + d.doubleValue();
        }
    }

在上一节中,我提到包装类是不可变的。那么当你向变量total(一个Double对象)添加值时会发生什么?在这种情况下,变量total引用了一个新的Double对象。

考试技巧

包装类是不可变的。向包装类变量添加原始值不会修改它所引用的对象的值。包装类变量被分配了一个新的对象。

这里还有一个有趣的问题。如果你将null作为以下方法的参数传递会发生什么?

public int increment(Integer obj) {
    return ++i;
}

因为 Java 编译器会调用obj.intValue()来获取objint值,所以将null传递给increment()方法将抛出NullPointerException

考试技巧

如果拆箱一个引用包装类变量,该变量引用null,将抛出NullPointerException

2.6. 概述

在本章中,我们首先介绍了 Java 中的基本数据类型,包括每种类型的用途示例及其文字面量。我们还把原始数据类型分为字符类型、整数类型和浮点类型。然后我们讨论了有效和无效 Java 标识符的要素。我们还介绍了原始类型和引用类型之间的区别。

我们讨论了用于操作原始数据类型的运算符(限于 OCA Java SE 8 程序员 I 考试所需的运算符)。我们还介绍了特定运算符可以使用的情况。例如,如果你希望检查一组条件是否为真,你可以使用逻辑运算符。了解每个运算符可以使用的操作数类型也很重要。例如,你不能使用boolean操作数与运算符>>=<=<一起使用。

我们讨论了包装类,包括它们的类层次结构、创建它们的实例、检索包装类实例存储的基本值、将字符串值解析为基本类型,以及比较包装类实例。在本章末尾,我们介绍了自动装箱和拆箱。

2.7. 复习笔记

基本数据类型:

  • Java 定义了八个基本数据类型:charbyteshortintlongfloatdoubleboolean

  • 基本数据类型是最简单的数据类型。

  • 基本数据类型是由编程语言预定义的。在 Java 中,用户不能定义基本数据类型。

  • 将基本数据类型分类为布尔型、数值型和字符型是有帮助的。

boolean数据类型:

  • boolean数据类型用于存储只有两种可能值的数。这两种可能的值可以认为是是/否、0/1、真/假,或者任何其他组合。boolean可以存储的实际值是truefalse

  • truefalse是文字面量。

  • 文字面量是一个固定值,不需要进一步计算就可以分配给任何变量。

数值数据类型:

  • 数值可以存储为整数或小数。

  • byteshortintlong可以用来存储整数。

  • byteshortintlong数据类型分别使用 8、16、32 和 64 位来存储它们的值。

  • floatdouble可以用来存储十进制数。

  • floatdouble数据类型分别使用 32 和 64 位来存储它们的值。

  • 整数的默认类型(即非十进制数)是int

  • 要将整数字面值指定为long值,请在字面值后添加后缀Ll

  • 数值可以存储在二进制、八进制、十进制和十六进制数格式中。这次考试不会要求你将一个数字从一个数制转换为另一个数制。

  • 十进制数系统中的字面值使用从 0 到 9 的数字(总共 10 个数字)。

  • 八进制数系统中的字面值使用从 0 到 7 的数字(总共 8 个数字)。

  • 十六进制数系统中的字面值使用从 0 到 9 的数字和从 A 到 F 的字母(总共 16 个数字和字母)。

  • 二进制数系统中的字面值使用数字 0 和 1(总共 2 个数字)。

  • 八进制数系统中的字面值以前缀0开头。例如,八进制数系统中的0413在十进制数系统中是267

  • 十六进制数系统中的字面值以前缀0x开头。例如,十六进制数系统中的0x10B在十进制数系统中是267

  • 二进制数系统中的字面值以前缀0b0B开头。例如,十进制值267在二进制系统中是0B100001011

  • 从 Java 7 开始,你可以在 Java 字面值中使用下划线来使它们更易于阅读。0B1_0000_10_110_4130x10_B是有效的二进制、八进制和十六进制字面值。

  • 十进制数的默认类型是double

  • 要将十进制字面值指定为float值,请在字面值后添加后缀Ff

  • 后缀Dd可以用来标记字面值为double值。虽然这样做是允许的,但不是必需的,因为十进制字面值的默认值是double

字符原始数据类型:

  • char数据类型可以存储单个 16 位 Unicode 字符;也就是说,它可以存储几乎所有世界现有脚本和语言中的字符。

  • 你可以使用从\u0000(或 0)到最大\uffff(或 65,535 inclusive)的值来存储一个char。Unicode 值是在十六进制数系统中定义的。

  • 在内部,char数据类型以无符号整数值的形式存储(只有正整数)。

  • 当你将一个字母分配给char时,Java 存储其整数等效值。你可以将一个正整数值分配给char而不是字母,例如 122。

  • 字面值122与 Unicode 值\u0122不同。前者是十进制数,后者是十六进制数。

  • 单引号,而不是双引号,用于将一个字母分配给char变量。

有效的标识符:

  • 一个有效的标识符以字母(a–z,大写或小写)、货币符号或下划线开头。其长度没有限制。

  • 一个有效的标识符可以包含数字,但不能位于起始位置。

  • 一个有效的标识符可以在标识符的任何位置使用下划线和货币符号。

  • 一个有效的标识符不能与 Java 关键字有相同的拼写,例如switch

  • 一个有效的标识符不能使用任何特殊字符,包括!@#%^&*()':;、``、/\}

赋值运算符:

  • 赋值运算符可以用来为所有类型的变量赋值或重新赋值。

  • 变量不能被赋值为不兼容的值。例如,字符和数值不能赋值给boolean变量,反之亦然。

  • +=-=是加法/减法和赋值的简写形式。

  • +=可以读作“先加后赋”,而-=可以读作“先减后赋”。

算术运算符:

  • 算术运算符不能与boolean数据类型一起使用。尝试这样做将使代码无法编译。

  • ++--是一元递增和递减运算符。这些运算符与单个操作数一起使用。

  • 一元运算符可以用前缀或后缀表示法使用。

  • 当使用一元运算符++--的前缀表示法时,变量的值在变量在表达式中使用前立即增加/减少。

  • 当使用一元运算符++--的后缀表示法时,变量的值在变量在表达式中使用后立即增加/减少。

  • 默认情况下,一元运算符的优先级高于乘法运算符和加法运算符。

关系运算符:

  • 关系运算符用于比较值是否相等(==)或不相等(!=),还用于确定两个数值是否大于(>>=)或小于(<<=)对方。

  • 您不能比较不可比较的值。例如,您不能比较booleanintchar或浮点数。如果您尝试这样做,您的代码将无法编译。

  • 等于(==)和不等于(!=)运算符可以用来比较所有类型的原始数据类型:charbyteshortintlongfloatdoubleboolean

  • 运算符==如果正在比较的原始值相等则返回true

  • 运算符!=如果正在比较的原始值不相等则返回true

  • 关系运算符的结果始终是一个布尔值。

逻辑运算符:

  • 您可以使用逻辑运算符来确定一组条件是否为真或假,并据此进行操作。

  • 逻辑AND&&)如果所有操作数都是true则返回true,否则返回false

  • 逻辑OR||)如果任何或所有操作数都是true则返回true

  • 逻辑非(!)否定布尔值。对于false返回true,反之亦然。

  • 逻辑运算的结果始终是一个 boolean 值。

  • 逻辑运算符 &&|| 也被称为短路运算符。如果这些运算符可以通过评估第一个操作数来确定表达式的输出,则它们不会评估第二个操作数。

  • 只有当两个操作数都为 true 时,&& 运算符才返回 true。如果此运算符的第一个操作数评估为 false,则结果永远不会是 true。因此,&& 不会评估第二个操作数。

  • 类似地,如果运算符的任一操作数是 true,则 || 运算符返回 true。如果此运算符的第一个操作数评估为 true,则结果永远不会是 false。因此,|| 不会评估第二个操作数。

包装类:

  • 包装类用于将原始值包装在对象中,因此可以将它们添加到集合对象中。

  • 所有包装类都是不可变的。

  • 您可以通过多种方式创建所有包装类的对象:

    • 赋值—通过将原始值赋给包装类变量(自动装箱)

    • 构造函数—通过使用包装类构造函数

    • 静态方法—通过调用包装类的静态方法,如 valueOf()

  • 所有包装类(除了 Character)都定义了一个接受表示需要包装的原始值的 String 参数的构造函数。

  • 没有包装类定义无参数构造函数。

  • 您可以直接将原始值赋给其包装类类型的引用变量,这称为自动装箱。相反,当原始包装类对象转换为相应的原始值时,称为拆箱。

  • 所有包装类都定义了格式为 primitive Value() 的方法,其中术语 primitive 指的是确切的原始数据类型名称。

  • 要获取与字符串值对应的原始数据类型值,您可以使用静态实用方法 parseDataType,其中 DataType 指的是返回值的类型。

  • 当传递一个原始类型或 String 参数时,valueOf() 方法返回相应包装类的对象。

  • 您可以通过使用 equals 方法或比较运算符 == 来比较包装类的对象是否相等。

  • 方法 equals 总是对比包装实例存储的原始值,而 == 比较对象引用。运算符 == 如果被比较的变量引用相同的实例,则返回 true

  • Boolean 类的情况下,由于只有两个实例:静态常量 Boolean.TRUEBoolean.FALSE,因此缓存的实例可以直接访问。

  • Character 类缓存了值为 0127 的实例。类 ByteShortIntegerLong 缓存值为 -128127 的实例。对于 FloatDouble 包装类没有缓存的实例。

  • 包装类是不可变的。将原始值添加到包装类变量不会修改它所引用的对象的值。包装类变量被分配了一个新的对象。

  • 解包引用包装器变量,该变量引用null,将抛出NullPointerException

2.8. 样本考试问题

[Q2-1.

给定:

int myChar = 97;
int yourChar = 98;
System.out.print((char)myChar + (char)yourChar);

int age = 20;
System.out.print(" ");
System.out.print((float)age);

输出是什么?

  1. 195 20.0
  2. 195 20
  3. ab 20.0
  4. ab 20
  5. 编译错误
  6. 运行时异常

Q2-2.

以下代码的正确选项是哪些?

public class Prim {                                // line 1
    public static void main(String[] args) {       // line 2
        char a = 'a';                              // line 3
        char b = -10;                              // line 4
        char c = '1';                              // line 5
        integer d = 1000;                          // line 6
        System.out.println(++a + b++ * c - d);     // line 7
    }                                              // line 8
}                                                  // line 9
  1. 行 4 的代码编译失败。
  2. 行 5 的代码编译失败。
  3. 行 6 的代码编译失败。
  4. 行 7 的代码编译失败。

Q2-3.

以下代码的输出是什么?

public class Foo {
    public static void main(String[] args) {
        int a = 10;
        long b = 20;
        short c = 30;
        System.out.println(++a + b++ * c);
    }
}
  1. 611
  2. 641
  3. 930
  4. 960

Q2-4.

给定:

Boolean buy = new Boolean(true);
Boolean sell = new Boolean(true);

System.out.print(buy == sell);
boolean buyPrim = buy.booleanValue();
System.out.print(!buyPrim);

System.out.print(buy && sell);

输出是什么?

  1. falsefalsefalse
  2. truefalsetrue
  3. falsetruetrue
  4. falsefalsetrue
  5. 编译错误
  6. 运行时异常

Q2-5.

以下哪个选项包含正确的代码来声明和初始化存储整数的变量?

  1. bit a = 0;
  2. integer a2 = 7;
  3. long a3 = 0x10C;
  4. short a4 = 0512;
  5. double a5 = 10;
  6. byte a7 = -0;
  7. long a8 = 123456789;

Q2-6.

选择在// INSERT CODE HERE处插入的选项,将使以下代码输出值为11

public class IncrementNum {
    public static void main(String[] args) {
        int ctr = 50;
        // INSERT CODE HERE
        System.out.println(ctr % 20);
    }
}
  1. ctr += 1;
  2. ctr =+ 1;
  3. ++ctr;
  4. ctr = 1;

Q2-7.

以下代码的输出是什么?

int a = 10;
int b = 20;
int c = (a * (b + 2)) - 10-4 * ((2*2) - 6;
System.out.println(c);
  1. 218
  2. 232
  3. 246
  4. 编译错误

Q2-8.

以下代码的哪些行是正确的?

boolean b = false;
int i = 90;
System.out.println(i >= b);
  1. 代码打印true
  2. 代码打印false
  3. 代码打印90 >= false
  4. 编译错误

Q2-9.

检查以下代码并选择正确的选项:

public class Prim {                                          // line 1
    public static void main(String[] args) {                 // line 2
        int num1 = 12;                                       // line 3
        float num2 = 17.8f;                                  // line 4
        boolean eJavaResult = true;                          // line 5
        boolean returnVal = num1 >= 12 && num2 < 4.567       // line 6
                             || eJavaResult == true;
        System.out.println(returnVal);                       // line 7
    }                                                        // line 8
}                                                            // line 9
  1. 代码打印false

  2. 代码打印true

  3. 如果将行 6 的代码修改为以下内容,代码将打印true

  4. boolean returnVal = (num1 >= 12 && num2 < 4.567) || eJavaResult == true;
    
  5. 如果将行 6 的代码修改为以下内容,代码将打印true

  6. boolean returnVal = num1 >= 12 && (num2 < 4.567 || eJavaResult == false);
    

Q2-10.

给定:

boolean myBool = false;                               // line 1
int yourInt = 10;                                     // line 2
float hisFloat = 19.54f;                              // line 3
System.out.println(hisFloat = yourInt);               // line 4
System.out.println(yourInt > 10);                     // line 5
System.out.println(myBool = false);                   // line 6

结果是什么?

  1. true
    true
    false
    
  2. 10.0
    false
    false
    
  3. false
    false
    false
    
  4. 编译错误

2.9. 样本考试问题答案

Q2-1.

给定:

int myChar = 97;
int yourChar = 98;
System.out.print((char)myChar + (char)yourChar);

int age = 20;
System.out.print(" ");
System.out.print((float)age);

输出是什么?

  1. 195 20.0
  2. 195 20
  3. ab 20.0
  4. ab 20
  5. 编译错误
  6. 运行时异常

答案:a

说明:当使用char原始数据类型作为算术运算符的操作数时,其对应的 ASCII 值用于算术运算。尽管(char)myCharint变量myChar显式转换为char类型,但其值97用于算术运算。当将字面值20显式转换为float类型时,它以十进制数的形式输出其值,即20.0

Q2-2.

以下代码的正确选项是哪些?

public class Prim {                                // line 1
    public static void main(String[] args) {       // line 2
        char a = 'a';                              // line 3
        char b = -10;                              // line 4
        char c = '1';                              // line 5

        integer d = 1000;                          // line 6
        System.out.println(++a + b++ * c - d);     // line 7
    }                                              // line 8
}                                                  // line 9
  1. 行 4 的代码编译失败。
  2. 行 5 的代码编译失败。
  3. 行 6 的代码编译失败。
  4. 行 7 的代码编译失败。

答案:a, c, d

说明:选项(a)是正确的。行 4 的代码编译失败,因为你不能在不进行类型转换的情况下将负值赋给原始char数据类型。

选项(c)是正确的。没有名为“integer”的原始数据类型。有效的数据类型是intInteger(一个以大写I为特征的包装类)。

选项 (d) 是正确的。变量 d 在第 7 行未定义,因为它的声明在第 6 行未能编译。因此,使用变量 d 的算术表达式 (++a + b++ * c - d) 未能编译。在算术表达式中使用 char 数据类型的变量 c 没有问题。char 数据类型在内部存储为无符号整数值,可以在算术表达式中使用。

Q2-3.

以下代码的输出是什么?

public class Foo {
    public static void main(String[] args) {
        int a = 10;
        long b = 20;
        short c = 30;
        System.out.println(++a + b++ * c);
    }
}
  1. 611
  2. 641
  3. 930
  4. 960

答案:a

说明:与变量 a 一起使用的前缀增量运算符 (++) 将在它被用于表达式 ++a + b++ * c 之前增加其值。与变量 b 一起使用的后缀增量运算符 (++) 将在表达式 ++a + b++ * c 使用其初始值之后增加其值。

因此,表达式 ++a + b++ * c 使用以下值进行评估:

11 + 20 * 30

因为乘法运算符的优先级高于加法运算符,所以 2030 的值在将结果加到值 11 之前被相乘。示例表达式评估如下:

(++a + b++ * c)
= 11 + 20 * 30
= 11 + 600
= 611
考试技巧

虽然问题 2-2 和 2-3 似乎是在测试你对运算符的理解,但实际上它们测试的是不同的主题。问题 2-2 测试的是原始数据类型的名称。注意!真正的考试有很多这样的问题。一个可能看起来是在测试你对线程的理解的问题,实际上可能是在测试你使用 do-while 循环的能力!

Q2-4.

给定:

Boolean buy = new Boolean(true);
Boolean sell = new Boolean(true);
System.out.print(buy == sell);

boolean buyPrim = buy.booleanValue();
System.out.print(!buyPrim);

System.out.print(buy && sell);

输出是什么?

  1. falsefalsefalse
  2. truefalsetrue
  3. falsetruetrue
  4. falsefalsetrue
  5. 编译错误
  6. 运行时异常

答案:d

说明:使用构造函数创建的 Boolean 实例 buysell。构造函数不引用缓存中的现有实例;它们创建新的实例。因为比较运算符 == 比较的是对象引用,而不是包装实例存储的基本值,所以 buy == sell 返回 false

可以使用 booleanValue() 方法来获取由布尔包装实例存储的基本 boolean 值。因此,buy.booleanValue() 返回 false。因为包装实例可以与算术和逻辑运算符一起使用,所以 buy && sell 可以编译,返回 true

Q2-5.

以下哪个选项包含正确代码来声明和初始化存储整数的变量?

  1. bit a = 0;
  2. integer a2 = 7;
  3. long a3 = 0x10C;
  4. short a4 = 0512;
  5. double a5 = 10;
  6. byte a7 = -0;
  7. long a8 = 123456789;

答案:c, d, f, g

说明:选项 (a) 和 (b) 是错误的。Java 中没有名为 bitinteger 的原始数据类型。正确的名称是 byteint

选项 (c) 是正确的。它将十六进制字面值赋给变量 a3

选项 (d) 是正确的。它将八进制字面值赋给变量 a4

选项(e)是错误的。它定义了一个类型为double的变量,用于存储小数,而不是整数。

选项(f)是正确的。-0是一个有效的字面量值。

选项(g)是正确的。123456789是一个有效的整数字面量值,可以被赋给类型为long的变量。

Q2-6.

选择在// INSERT CODE HERE处插入的选项,以使以下代码输出值为11

public class IncrementNum {
    public static void main(String[] args) {
        int ctr = 50;
        // INSERT CODE HERE
        System.out.println(ctr % 20);
    }
}
  1. ctr += 1;
  2. ctr =+ 1;
  3. ++ctr;
  4. ctr = 1;

答案:a, c

说明:为了输出11的值,变量ctr的值应该是51,因为51%2011。运算符%输出除法操作的余数。变量ctr的当前值是50。可以使用正确的赋值或增量运算符将其增加1

选项(b)是错误的。Java 没有定义=+运算符。正确的运算符是+=

选项(d)是错误的,因为它将变量result的值赋为1,而不是将其增加1

Q2-7.

以下代码的输出是什么?

int a = 10;
int b = 20;
int c = (a * (b + 2)) - 10-4 * ((2*2) - 6;
System.out.println(c);
  1. 218
  2. 232
  3. 246
  4. 编译错误

答案:d

说明:首先,每次回答任何使用括号来覆盖运算符优先级的题目时,检查开括号的数量是否与闭括号的数量匹配。这段代码将无法编译,因为开括号的数量不匹配闭括号的数量。

其次,你可能不需要在真正的考试中回答复杂的表达式。每次看到过于复杂的代码时,都要寻找代码中的其他可能问题。复杂的代码可能被用来分散你的注意力,从而掩盖真正的问题。

Q2-8.

以下代码行有什么正确之处?

boolean b = false;
int i = 90;
System.out.println(i >= b);
  1. 代码打印true
  2. 代码打印false
  3. 代码打印90 >= false
  4. 编译错误

答案:d

说明:这段代码将无法编译;因此,它无法执行。你不能比较不可比较的类型,例如boolean值与数字。

Q2-9.

检查以下代码并选择正确的选项:

public class Prim {                                          // line 1
    public static void main(String[] args) {                 // line 2
        int num1 = 12;                                       // line 3
        float num2 = 17.8f;                                  // line 4

        boolean eJavaResult = true;                          // line 5
        boolean returnVal = num1 >= 12 && num2 < 4.567       // line 6
                             || eJavaResult == true;
        System.out.println(returnVal);                       // line 7
    }                                                        // line 8
}                                                            // line 9
  1. 代码打印false

  2. 代码打印true

  3. 如果将第 6 行的代码修改为以下内容,代码将打印true

  4. boolean returnVal = (num1 >= 12 && num2 < 4.567) || eJavaResult == true;
    
  5. 如果将第 6 行的代码修改为以下内容,代码将打印true

  6. boolean returnVal = num1 >= 12 && (num2 < 4.567 || eJavaResult == false);
    

答案:b, c

说明:选项(a)是错误的,因为代码打印true

选项(d)是错误的,因为代码打印false

选项(c)中的代码使用括号来指示哪个表达式应该先于其他表达式评估。以下是执行步骤:

boolean returnVal = (num1 >= 12 && num2 < 4.567) || eJavaResult == true;
returnVal = false || eJavaResult == true;
returnVal = true;

题目中的原始代码没有使用括号来分组表达式。在这种情况下,因为运算符&&的优先级高于||,表达式'num1 >= 12 && num2 < 4.567'将是首先执行的表达式。以下是执行步骤:

boolean returnVal = num1 >= 12 && num2 < 4.567 || eJavaResult == true;
returnVal = false || eJavaResult == true;
returnVal = true;

Q2-10.

给定:

boolean myBool = false;                               // line 1
int yourInt = 10;                                     // line 2
float hisFloat = 19.54f;                              // line 3
System.out.println(hisFloat = yourInt);               // line 4
System.out.println(yourInt > 10);                     // line 5
System.out.println(myBool = false);                   // line 6

结果是什么?

  1. true
    true
    false
    
  2. 10.0 false false
  3. false
    false
    false
    
  4. 编译错误

答案:b

解释:表达式 myBool = false 使用的是赋值运算符(=),而不是比较运算符(==)。这个表达式将布尔字面量 false 赋值给 myBool;它并不是在比较 falsemyBool。在考试中要留意类似的(技巧性)赋值,它们可能 看起来 是在比较值。

第三章。方法和封装

本章涵盖的考试目标 你需要了解的内容
[1.1] 定义变量的作用域。 变量可以有多个作用域:类、实例、方法和局部。在给定作用域中变量的可访问性。
[2.3] 了解如何读取或写入对象字段。 可以通过直接访问实例变量和调用方法来读取和写入对象字段。调用对象上方法的正确符号。方法可能或可能不会改变实例变量的值。访问修饰符影响通过引用变量可以调用的实例变量和方法的可访问性。非静态方法不能在未初始化的对象上调用。
[2.4] 解释对象的生命周期(创建、通过重新赋值进行“解引用”和垃圾回收)。 对象声明、初始化、可访问性和 Java 垃圾回收器可以收集对象的条件之间的差异。Java 中的垃圾回收。
[6.1] 创建具有参数和返回值的函数;包括重载函数。 创建具有正确返回类型和方法参数列表的函数。创建具有相同名称但参数列表不同的函数。
[6.3] 创建和重载构造函数;包括对默认构造函数的影响。 正如常规方法一样,构造函数可以重载。默认构造函数与无参数构造函数不同。当没有创建用户定义的构造函数时,Java 定义了一个无参数构造函数。用户定义的构造函数可以重载。
[6.5] 将封装原则应用于类。 封装的需求和好处。正确实现封装原则的类的定义。
[6.6] 确定当将它们传递给会更改值的函数时,对象引用和原始值的影响。 当传递给方法时,对象引用和原始值被以不同的方式处理。与引用变量不同,当传递给方法时,原始值的值在调用方法中永远不会改变。

四处看看,你会发现许多良好封装的对象的例子。例如,我们中的大多数人使用银行的 服务,它应用一系列定义良好的流程,使我们能够保护我们的金钱和贵重物品(银行保险库)。银行可能需要我们从我们那里获取输入以执行其某些流程,例如将钱存入我们的账户。但是,银行可能或可能不会告诉我们其他流程的结果;例如,它可能在交易后告诉我们账户余额,但很可能不会告诉我们其为新员工招聘的计划。

在 Java 中,你可以将银行比作一个封装良好的类,而银行流程则对应 Java 方法。在这个类比中,你的钱和贵重物品就像 Java 中的对象字段。你还可以将银行流程所需的输入与 Java 的方法参数进行比较,并将银行流程的结果与 Java 方法的返回值进行比较。最后,你可以将银行在开设账户时执行的步骤集合与 Java 中的构造函数进行比较。

在考试中,你必须回答有关方法和封装的问题。本章将帮助你通过涵盖以下内容来获得正确答案:

  • 定义变量的作用域

  • 解释对象的生命周期

  • 创建具有原始类型和对象参数以及返回值的方法

  • 创建重载方法和构造函数

  • 读取和写入对象字段

  • 在对象上调用方法

  • 将封装原则应用于类

让我们开始讨论变量的作用域。

3.1. 变量的作用域

[1.1] 定义变量的作用域

变量的作用域指定了其生命周期及其可见性。在本节中,我们将介绍变量的作用域,包括它们可访问的域。以下是可用的变量作用域:

  • 局部变量(也称为方法局部变量)

  • 方法参数(也称为方法参数)

  • 实例变量(也称为属性、字段和非静态变量)

  • 类变量(也称为静态变量)

通常情况下,变量的作用域在定义它的代码块括号关闭时结束。现在这可能难以理解,但当你通过示例学习后,它将变得更加清晰。让我们从定义局部变量开始。

3.1.1. 局部变量

局部变量是在方法内部定义的。它们可能或可能不在诸如if-else结构、循环结构或switch语句等代码结构内部定义。通常,你会使用局部变量来存储计算的中间结果。与之前列出的其他三个变量作用域相比,它们的作用域(生命周期)最短。

在以下代码中,局部变量avg是在get-Average()方法内部定义的:

如你所见,在getAverage方法中局部定义的变量avg不能在它外部,即setAverage方法中访问。这个局部变量avg的作用域在图 3.1 中有所描述。未阴影区域表示avg可访问的区域,而阴影区域表示它不可用的区域。

注意

变量的生命周期由其作用域决定。如果变量的作用域仅限于一个方法,那么其生命周期也仅限于该方法。你可能注意到这些术语可以互换使用。

图 3.1. 你只能在getAverage方法中访问局部变量 avg。

让我们定义另一个变量,avg,它是if语句块(当if条件评估为true时执行的代码)的局部变量:

在这种情况下,局部变量avg的作用域缩小到getAverage方法内定义的if-else语句的if块。这个局部变量avg的作用域在图 3.2 中展示,其中无阴影区域表示avg可访问的地方,阴影部分表示不可用的地方。

图 3.2. 局部变量avg的作用域是if语句的一部分。

同样,循环变量在循环体外部是不可访问的:

考试技巧

局部变量是 OCA Java SE 8 程序员 I 考试作者们偏爱的主题。你可能会被问到关于继承或异常处理等相对复杂主题的问题,但实际测试的将是你对局部变量作用域的知识。

在声明之前能否访问局部变量?不可以。不允许对局部变量的前向引用

如果你将前例中变量的声明顺序颠倒,代码将可以编译:

局部变量的作用域取决于其在方法中声明的位置。在循环、if-elseswitch构造或代码块(用{}标记)内定义的局部变量的作用域仅限于这些构造。在上述构造之外定义的局部变量在整个方法中都是可访问的。

下一个部分将讨论方法参数的作用域。

3.1.2. 方法参数

在方法签名中接受值的变量被称为方法参数。它们只能在定义它们的方法中访问。在以下示例中,为setTested方法定义了一个方法参数val

在前面的代码中,你只能在setTested方法内访问方法参数val。在其他任何方法中都无法访问。

方法参数val的作用域在图 3.3 中展示。无阴影区域表示变量可访问的地方,阴影部分表示变量不可用的地方。

图 3.3. 在方法setTested中定义的方法参数val的作用域

方法参数的作用域可能和局部变量一样长,甚至更长,但它永远不会更短。以下isPrime方法定义了一个方法参数num和两个局部变量resultctr

方法参数 num 的范围与局部变量 result 的范围相同。因为局部变量 ctr 的范围仅限于 for 块,所以它比方法参数 num 短。这三个变量范围的比较在 图 3.4 中显示,其中每个变量的范围(在一个椭圆中定义)由包围它的矩形表示。

图 3.4

图 3.4

让我们继续讨论实例变量,它们的范围比方法参数更大。

3.1.3. 实例变量

实例 是对象的另一个名称。因此,实例变量 在对象的生命周期内可用。实例变量在类中声明,位于所有方法之外。它可被类中定义的所有实例(或非静态)方法访问。

在下面的示例中,变量 tested 是一个实例变量——它在类 Phone 中定义,位于所有方法之外。它可以被类 Phone 的所有方法访问:

图 3.1

实例变量 tested 的范围在 图 3.5 中表示。如图所示,变量 tested 可以跨 Phone 类的对象访问,由无阴影区域表示。它在 setTestedisTested 方法中可访问。

图 3.5. 实例变量 tested 可以跨 Phone 类的对象访问。

图 3.5

考试技巧

实例变量的范围比局部变量或方法参数的范围更长。

在下一节中介绍的类变量是所有变量类型中范围最大的。

3.1.4. 类变量

一个 类变量 是通过使用关键字 static 定义的。类变量属于一个类,而不是属于类的单个对象。类变量在所有对象之间共享——对象没有类变量的单独副本。

您甚至不需要一个对象来访问类变量。您可以使用定义它的类的名称来访问它:

图 3.2

让我们尝试在另一个类中访问这个变量:

图 3.1

如前述代码所示,类变量 softKeyboard 可以通过以下所有方式访问:

  • Phone.softKeyboard

  • p1.softKeyboard

  • p2.softKeyboard

无论您使用类的名称(Phone)还是对象的引用(p1)来访问类变量,都没有关系。您可以使用它们中的任何一个来更改类变量的值,因为它们都指向单个共享副本。当您使用 null 引用来访问静态变量 softKeyboard 时,Java 会引用引用变量 p1p2类型(即 Phone),而不是它们所引用的对象。因此,使用 null 引用访问静态变量不会抛出异常:

图 3.2

类变量softKeyboard的作用域在图 3.6 中展示。如图所示,这个变量的单个副本对所有Phone类的对象都是可访问的。即使没有Phone实例的存在,softKeyboard变量也是可访问的。类变量softKeyboard在 JVM 将Phone类加载到内存时由 JVM 使其可访问。类变量softKeyboard的作用域取决于其访问修饰符和Phone类的访问修饰符。因为Phone类和类变量softKeyboard都是使用默认访问定义的,所以它们只能在包com.mobile内访问。

图 3.6。类变量softKeyboard的作用域限制在包com.mobile内,因为它是在使用默认访问定义的Phone类中定义的。类变量softKeyboardPhone类的所有对象之间共享并可访问。

图片

比较不同作用域中变量的使用

这里是对局部变量、方法参数、实例变量和类变量使用的快速比较:

  • 局部变量是在方法内定义的,通常用于存储计算的中间结果。

  • 方法参数用于将值传递给方法。这些值可以被操作,也可以分配给实例变量。

  • 实例变量用于存储对象的状态。这些是需要被多个方法访问的值。

  • 类变量用于存储应该由类中所有对象共享的值。

3.1.5。重叠变量作用域

在前面的关于局部变量、方法参数、实例变量和类变量的章节中,你是否注意到一些变量在对象内的多个地方都是可访问的?例如,所有四个变量在方法内的循环中都是可访问的。

这种重叠的作用域在图 3.7 中展示。变量定义在椭圆形中,并且可以在所有方法和块内访问,如它们包围的矩形所示。

图 3.7。变量的作用域可以重叠。

图片

如图 3.7 所示,一个类的classVariable实例可以被该类的多个对象(object1object2)访问和共享。object1object2各自都有自己的实例变量instanceVariable的副本,因此instanceVariable可以在object1的所有方法中访问。当与object1object2一起使用时,method1method2各自有自己的localVariablemethodParameter副本。

注意

instanceVariable的作用域与在method1中定义的local-VariablemethodParameter的作用域重叠。因此,这三个变量(instanceVariablelocalVariablemethodParameter)可以在重叠区域相互访问。但是instanceVariable不能在method1外部访问localVariablemethodParameter

比较变量的作用域

图 3.8 比较了局部变量、方法参数、实例变量和类变量的生命周期。

图 3.8. 比较所有四个变量的作用域或生命周期

如图 3.8 所示,局部变量的作用域或生命周期最短,类变量的作用域或生命周期最长。

考试提示

不同的局部变量可以有不同的作用域。局部变量的作用域可能短于或等于方法参数的作用域。如果局部变量在方法中的子块(在大括号{}内)中声明,则局部变量的作用域小于方法的作用域。这个子块可以是一个if语句、一个switch构造、一个循环或一个try-catch块(在第七章中讨论)。

不同作用域中具有相同名称的变量

变量作用域的重叠导致在不同作用域内具有相同名称的变量组合变得有趣。一些规则是必要的,以防止冲突。特别是,你无法在类中定义具有相同名称的static变量和实例变量:

同样,局部变量和方法参数不能具有相同的名称。以下代码定义了一个方法参数和一个局部变量,它们具有相同的名称,因此无法编译:

一个类可以定义与实例或类变量具有相同名称的局部变量,也称为阴影。以下代码定义了一个类变量和一个局部变量softKeyboard,它们具有相同的名称,以及一个实例变量和一个局部变量phoneNumber,它们也具有相同的名称,这是可接受的:

注意

在重叠的作用域中定义具有相同名称的变量可能是一种危险的编码实践。这通常只在非常特定的情况下被接受,例如构造函数和设置器。请编写易于阅读、理解和维护的代码。

当你给一个与实例变量具有相同名称的局部变量赋值时会发生什么?实例变量会反映这个修改后的值吗?这个问题为本章“故事中的第一个转折”练习提供了思考的食物。它应该有助于你记住当你给类中已经存在具有相同名称的实例变量赋值时,给局部变量赋值会发生什么(答案见附录)。

故事中的转折 3.1

Phone 定义了一个局部变量和一个实例变量,phoneNumber,具有相同的名称。检查方法 setNumber 的定义。在您的系统上执行该类,并从给定选项中选择类 TestPhone 的正确输出:

class Phone {
    String phoneNumber = "123456789";
    void setNumber () {
        String phoneNumber;
        phoneNumber = "987654321";
    }
}
class TestPhone {
    public static void main(String[] args) {
        Phone p1 = new Phone();
        p1.setNumber();
        System.out.println (p1.phoneNumber);
    }
}
  1. 123456789

  2. 987654321

  3. 无输出

  4. Phone 将无法编译。

在本节中,你使用了不同作用域中的变量。当变量超出作用域时,它们就不再被剩余的代码访问。在下一节中,你将看到对象是如何创建和变得可访问,然后又变得不可访问的。

3.2. 对象的生命周期

[2.4] 解释对象的生命周期(创建、“通过重新赋值取消引用”和垃圾回收)

OCA Java SE 8 程序员 I 考试将测试你对对象何时创建、何时可访问以及何时可取消引用的理解。考试还将测试你确定特定代码行处可访问对象总数的能力。原始数据类型不是对象,因此在本节中不相关。

与一些其他编程语言,如 C 语言不同,Java 不允许你在创建或销毁对象时自行分配或释放内存。Java 管理分配对象和回收未使用对象占用的内存。

回收未使用内存的任务由 Java 的垃圾回收器处理,它是一个低优先级的线程。它定期运行并释放未使用对象占用的空间。

Java 还提供了一个名为 finalize 的方法,该方法对所有类都是可访问的。finalize 方法在类 java.lang.Object 中定义,这是所有 Java 类的基类。所有 Java 类都可以重写 finalize 方法,该方法在对象被垃圾回收之前执行。理论上,你可以使用此方法释放对象使用的资源,尽管这样做并不推荐,因为其执行并不保证会发生。

对象的生命周期从其创建开始,一直持续到它超出作用域或不再被变量引用。当对象可访问时,它可以被变量引用,其他类可以通过调用其方法和访问其变量来使用它。我将在以下子节中详细讨论这些阶段。

3.2.1. 一个对象诞生

当你使用关键字操作符 new 时,对象就会出现。你可以使用此对象初始化一个引用变量。注意声明变量和初始化它的区别。以下是一个 Person 类和一个 ObjectLifeCycle 类的示例:

在前面的代码中,类 ObjectLifeCycle 中没有创建 Person 类的对象;它只声明了一个 Person 类型的变量。当引用变量被初始化时,才会创建对象:

变量声明和对象创建的区别在图 3.9 中得到了说明,你可以将婴儿的名字与引用变量相比,将真正的婴儿与对象相比。图 3.9 中的左框代表变量声明,因为婴儿还没有出生。图 3.9 中的右框代表对象创建。

图 3.9. 声明引用变量和初始化引用变量的区别

图片

从语法上讲,对象是通过使用new运算符创建的。但String类在这里是一个特例。String引用变量也可以通过使用字符串字面值来初始化:

图片

注意

初始化引用变量和实例化不是一回事。初始化引用变量不一定总是导致创建新的实例。在第四章中,我们将详细讲解 JVM 如何将String 字面值池化到字符串池中。尽管使用new运算符总是创建一个新的String对象,但使用String字面值来初始化String引用变量不一定总是创建一个新的String对象。

当你创建一个新对象而没有将其分配给任何引用变量时会发生什么?让我们在类ObjectLifeCycle2中创建一个新对象Person,但不将其分配给任何引用变量(粗体部分为修改):

图片

在前面的例子中,创建了一个Person类的对象,但无法使用任何引用变量来访问它。以这种方式创建对象将执行类的相关构造函数。

考试技巧

注意在给定代码中创建的实例的数量——那些可以和不可以进行垃圾回收的实例。

在下一节中,你将了解对象创建后会发生什么。

3.2.2. 对象是可访问的

一旦创建了对象,就可以使用其引用变量来访问它。它将保持可访问状态,直到它超出作用域或其引用变量被显式设置为null。此外,如果你将另一个对象重新分配给已初始化的引用变量,则之前的对象将从这个变量中变得不可访问。你可以在其他类和方法中访问和使用对象。

看一下以下Exam类的定义:

class Exam {
    String name;
    public void setName(String newName) {
        name = newName;
    }
}

ObjectLife1类声明了一个Exam类型的变量,创建了它的对象,调用了它的方法,将其设置为null,然后重新初始化它:

图片

前面的例子使用相同的引用变量myExam创建了两个Exam类的对象。让我们分析一下例子中发生的情况:

  • 图片 创建了一个引用变量myExam,并用Exam类的对象初始化它。

  • 图片 在变量myExam引用的对象上调用setName方法。

  • ![num-3.jpg] 将值null分配给引用变量myExam,这样通过myExam就无法访问该变量所引用的对象。

  • ![num-4.jpg] 创建了一个新的Exam类的对象并将其分配给引用变量myExam

  • ![num-5.jpg] 在方法main中创建的第二个Exam对象上调用setName方法。

当![num-4.jpg] 创建另一个Exam类的对象并将其分配给变量myExam时,第一个由![num-1.jpg] 创建的对象会发生什么?因为第一个对象无法通过任何变量访问,所以 Java 认为它是垃圾,并认为它应该由 Java 的垃圾回收器送入垃圾箱。如前所述,垃圾回收器是一个低优先级的线程,它回收 Java 中未使用或未引用的对象所占用的空间。

当对象变得不可访问时会发生什么?你将在下一节中找到答案。

3.2.3. 对象不可访问

如果对象超出作用域或通过重新分配被取消引用,它可能变得不可访问。

变量超出作用域

如果对象超出作用域,它可能变得不可访问:

![162fig01_alt.jpg]

在前面的代码中,变量myExam1是在if块内定义的局部变量。它的作用域从声明它的行开始,直到以闭合花括号![num-1.jpg] 标记的if块的末尾。在这个闭合花括号之后,变量myExam1所引用的对象不再可访问。它超出作用域,并被 Java 的垃圾回收器标记为符合垃圾回收的条件。同样,变量myExam2所引用的对象在以闭合花括号![num-3.jpg] 标记的else块末尾变得不可访问。

考试技巧

当对象超出作用域时,它将无法被引用,并被标记为垃圾回收。

通过重新分配取消引用

已经引用实例的变量可以被分配另一个实例。在这种情况下,较早的实例被取消引用,并符合垃圾回收的条件。让我们用一个修改过的先前代码示例来工作:

![162fig02_alt.jpg]

在前面的代码中,创建了一个Exam实例并将其分配给变量myExam ![num-1.jpg]。在![num-2.jpg] myExam被分配另一个Exam实例之前被设置为null ![num-3.jpg]。![num-4.jpg] 中的代码重新分配了另一个Exam实例给myExam,而没有明确将其设置为null。同样,在![num-3.jpg] 创建的实例再次被取消引用。在![num-4.jpg] 执行后,两个MyExam实例通过重新分配被取消引用,并符合垃圾回收的条件。

在![num-5.jpg],另一个变量yourExam使用Exam实例初始化。在![num-6.jpg],变量myExam被分配给变量yourExam。这取消了之前分配给yourExamExam实例的引用。

图 3.10 展示了Exam实例如何通过变量myExamyourExam被引用。使用灰色框突出显示的Exam实例代表未引用的对象。

图 3.10. 对象可以通过变量的重新赋值来解除引用。

图片

考试技巧

当一个变量被显式设置为null或分配给另一个实例或引用变量时,实例通过重新赋值来解除引用。

3.2.4. 垃圾回收

在 OCA Java SE 8 程序员 I 级考试中,你可能会回答有关具有多个变量声明和初始化的代码的垃圾回收问题。考试可能会询问在特定代码行之后有多少对象有资格进行垃圾回收。

自动内存管理

垃圾回收器是一个低优先级的线程,它在 JVM 中标记有资格进行垃圾回收的对象,然后清除这些对象的内存。它通过程序员不需要自己标记这些实例来实现自动内存管理。

何时进行垃圾回收?

你只能确定哪些对象有资格进行垃圾回收。你永远无法确定特定对象何时会被垃圾回收。用户无法控制或确定垃圾回收器的执行。它由 JVM 控制。

考试技巧

注意那些措辞如“在下一个 GC 周期中哪些对象一定会被收集”的问题,对于这类问题的真正答案永远无法得知。

让我们回顾一下我在第二章中使用的狗和狗绳的类比来定义对象引用变量。在图 3.11 中,你可以将对象引用变量与狗绳以及对象与狗进行比较。回顾以下比较,这将帮助你理解对象的生命周期和垃圾回收:

  • 一个未初始化的引用变量可以比作没有狗的狗绳。

  • 一个已初始化的引用变量可以比作拴着的狗。

  • 一个未引用的对象可以比作未拴的狗。

图 3.11. 比较对象引用变量和对象与狗绳和拴着的狗以及未拴的狗

图片

你可以将 Java 的垃圾回收器比作动物控制。动物控制收集未拴狗的方式就像 Java 的垃圾回收器回收未引用对象使用的内存一样。

使用System.gc()Runtime.getRuntime().gc()

作为程序员,你不能启动 Java 垃圾回收器的执行。你只能通过调用System.gc()Runtime.getRuntime().gc()来请求它启动。但调用此方法并不保证垃圾回收器何时会启动(JVM 甚至可以忽略这个调用)。注意那些询问你在调用System.gc()之后有多少实例已被垃圾回收的考试问题。它不会在任何代码行保证任何计数。

垃圾回收引用对象

垃圾回收器还可以从一组引用对象中回收内存。这组变量被称为 隔离岛

一个实例可以被多个变量引用。因此,当你将这些变量之一设置为 null 时,实例仍然可以通过其他变量(s)进行引用。但是,没有 外部引用 的一组实例符合垃圾回收的条件。让我们通过一个例子来操作:

在前面的示例中,一个 Exam 实例可以使用其字段 other 指向其自身类型的对象。在 处,创建了两个变量 phpjava,并使用 Exam 实例进行初始化。在 处,java 被赋值给 php.other。在 处,php 被赋值给 java.other。在 处,当 php 被设置为 null 时,它所引用的实例 符合垃圾回收的条件,因为它仍然可以通过 java.other 进行引用。在 处,当 java 也被设置为 null 时,javaphp 所引用的对象都符合垃圾回收的条件。如图 图 3.12 所示,尽管这两个对象可以相互引用,但它们在 main 方法中不能再被引用。它们形成了一个 隔离岛。Java 的垃圾回收器可以确定这样的实例组。

图 3.12. 没有外部引用的一组实例形成一个隔离岛,符合垃圾回收的条件。

现在你已经熟悉了对象的生命周期,你可以创建接受原始数据类型和对象作为方法参数的方法;这些方法返回一个值,这个值可以是原始数据类型或对象。

3.3. 创建具有参数和返回值的方法

[6.1] 创建具有参数和返回值的方法;包括重载方法

在本节中,你将处理方法的定义,这些方法可能接受输入参数,也可能不返回任何值。

方法是一组与名称相关的语句。方法用于定义对象的行为。方法可以执行不同的操作,如图 图 3.13 所示。

图 3.13. 不同类型的方法

  1. setModel 方法可以访问并修改 Phone 实例的状态。

  2. printVal 方法仅使用传递给它的方法参数。

  3. todaysDate 方法初始化一个 java.util.Date 实例,并返回其 String 表示形式。

在接下来的小节中,你将了解方法组件:

  • 返回类型

  • 方法参数

  • return 语句

  • 访问修饰符(在第一章中介绍)

  • 非访问修饰符(在第一章中介绍)

图 3.14 展示了一个接受方法参数并定义返回类型和 return 语句的方法的代码。

让我们从讨论方法的返回类型开始。

图 3.14. 接受方法参数并定义返回类型和 return 语句的方法示例

3.3.1. 方法的返回类型

方法的返回类型声明了方法将返回的值的类型。一个方法可能返回值,也可能不返回值。不返回值的方法具有 void 返回类型。方法可以返回一个原始值或任何类的对象。返回类型的名称可以是 Java 中定义的八个原始类型之一、一个类或一个接口。

在以下代码中,方法 setWeight 不返回任何值,而方法 getWeight 返回一个值:

如果一个方法不返回值,你不能将该方法的结果赋给变量。你认为以下使用前面类 PhoneTestMethods 类的输出是什么?

上述代码无法编译,因为方法 setWeight 不返回任何值。它的返回类型是 void。因为方法 setWeight 不返回任何值,所以没有可以赋给变量 newWeight 的内容,因此代码无法编译。

如果一个方法返回一个值,调用该方法可能或可能不会费心将方法返回的值存储在变量中。看看以下代码:

在前面的例子中,方法 getWeight 返回的值没有被赋给任何变量,这对 Java 编译器来说不是问题。编译器会愉快地为你编译代码。

考试技巧

你可以选择将方法返回的值赋给变量。如果你没有将方法返回的值赋给变量,这既不是编译错误,也不是运行时异常。

你从方法返回的值必须可以赋值给它被赋值的变量。例如,Phone 中的 getWeight() 的返回值是 double。你可以将 getWeight() 的返回值赋给 double 类型的变量,但不能赋给 int 类型的变量(没有显式转换)。以下是代码:

在前面的代码中, 将成功编译,因为方法 getWeight 的返回类型是 double,而变量 newWeight 的类型也是 double。但 无法编译,因为从方法 getWeight 返回的 double 值不能赋给类型为 int 的变量 newWeight2。你可以通过显式转换使其发生:

但显式转换不适用于不兼容的数据类型:

我们已经讨论了如何从方法中传递一个值。要将值传递到方法中,你可以使用方法参数。

3.3.2. 方法参数

方法参数是出现在方法定义中的变量,并指定方法可以接受的数据类型和值的数量。在图 3.15 中,变量phNummsg是方法参数。

图 3.15. 一个接受方法参数并定义返回类型和return语句的方法示例

您可以将多个值传递给一个方法作为输入。理论上,没有限制可以定义的方法参数数量,但实际上定义超过三个方法参数并不是一个好主意。使用具有太多方法参数的方法很繁琐,因为您必须多次交叉检查它们的类型和目的,以确保您在正确的位置传递了正确的值。

注意

虽然术语方法参数方法参数并不相同,您可能已经注意到许多程序员将它们互换使用。方法参数是出现在方法定义中的变量。方法参数是在执行方法时传递给方法的实际值。在图 3.15 中,变量phNummsg是方法参数。如果您以sendMsg("123456", "Hello")的方式执行此方法,那么String"123456""Hello"是方法参数。如您所知,您可以将字面值或变量传递给方法。因此,方法参数可以是字面值或变量。

一个方法可以接受零个或多个方法参数。以下示例接受两个int值,并返回它们的平均值作为double值:

以下示例显示了一个不接受任何方法参数的方法:

void printHello() {
    System.out.println("Hello");
}

如果一个方法不接受任何参数,则方法名称后面的括号是空的。因为关键字void用于指定方法不返回值,您可能会认为使用关键字void来指定方法不接受任何方法参数是正确的,但实际上这是不正确的。以下是不接受参数的方法定义无效的示例:

您可以在方法中定义一个可以接受可变参数(varargs)的参数。以下是一个名为Employee的类的示例,它定义了一个名为days-OffWork的方法,该方法接受可变参数:

class Employee {
    public int daysOffWork(int... days) {
        int daysOff = 0;
        for (int i = 0; i < days.length; i++)
            daysOff += days[i];
        return daysOff;
    }
}

跟在数据类型后面的省略号(...)表示方法参数days可以传递一个数组或多个以逗号分隔的值。重新审查前面的代码示例,并注意在daysOffWork方法中变量days的使用——它就像一个数组。当您为方法定义可变长度参数时,Java 会在幕后创建一个数组来实现它。

在参数列表中,您只能定义一个可变参数,并且它必须是参数列表中的最后一个变量。如果您不遵守这两条规则,您的代码将无法编译:

如果你的方法定义了多个方法参数,接受可变参数的变量必须是参数列表中的最后一个:

考试技巧

在 OCA 考试中,你可能会被问及不接受任何方法参数的方法的有效返回类型。请注意,没有有效或无效的组合可以传递给方法的方法参数的数量和类型以及它可以返回的值。它们是相互独立的。

你可以向方法传递任何类型和数量的参数,包括基本类型、类的对象或接口引用的对象。

要记住的规则

在定义方法参数时需要注意以下事项:

  • 你可以为方法定义多个参数。

  • 方法参数可以是基本类型或对象。

  • 方法参数之间用逗号分隔。

  • 每个方法参数前面都跟着其类型的名称。每个方法参数必须使用其名称显式声明类型。你不能像变量那样先声明类型,然后用逗号分隔参数列表。

3.3.3. 返回语句

return语句用于从方法中退出,无论是否有值。对于定义了返回类型的方法,return语句必须立即后跟返回值。对于不返回值的方法,可以使用不带返回值的return语句来退出方法。图 3.16 说明了return语句的使用。

图 3.16. 接受方法参数并定义返回类型和return语句的方法示例

在这个例子中,我们将回顾之前关于方法calcAverage的例子,该方法使用return语句返回double类型的数据:

不返回值的方法(返回类型为void)不需要定义return语句:

即使方法不返回值,你仍然可以在方法中使用return语句。通常这个语句用于定义方法的早期退出:

此外,如果存在,return语句必须是方法中最后执行的最后一条语句。return语句将控制权从方法中移出,这意味着在它之后定义任何代码都没有意义。编译器将无法编译此类代码:

注意,方法中的return语句作为最后一条语句和方法中最后执行的最后一条语句之间存在差异。return语句不必是方法中的最后一条语句,但它必须是方法中最后执行的最后一条语句:

void setWeight(double val) {
    if (val < 0)
        return;
    else
        weight = val;
}

在前面的例子中,return语句不是这个方法中的最后一条语句。但它对于小于零的方法参数值是最后执行的最后一条语句。

定义返回语句时要记住的规则

定义return语句时需要注意以下事项:

  • 对于返回值的方法,return语句必须立即跟随着一个值。

  • 对于不返回值的方法(返回类型为void),return语句后面不得跟有返回值。

  • 如果编译器确定return语句不是方法中最后一个执行的语句,则方法将无法编译。

你认为我们已经涵盖了定义方法的所有规则了吗?还没有!你认为你可以在一个类中定义具有相同名称的多个方法吗?你可以,但你需要注意一些额外的规则,这些规则将在下一节中讨论。

3.4. 创建重载方法

[6.1] 创建具有参数和返回值的方法;包括重载方法

重载方法是具有相同名称但不同方法参数列表的方法。在本节中,你将学习如何创建和使用重载方法。

想象一下你正在做讲座,需要指导听众使用纸张、智能手机或笔记本电脑(当天他们可用的任何设备)做笔记。一种方法是为听众提供一个如下指令的列表:

  • 使用纸张做笔记。

  • 使用智能手机做笔记。

  • 使用笔记本电脑做笔记。

另一种方法是指导他们“做笔记”,然后提供他们应该使用的纸张、智能手机或笔记本电脑。除了后者方法的简单性之外,它还允许你在不需要记住所有指令列表的情况下,添加其他用于做笔记的媒体(如手、布或墙)。

这种第二种方法,提供一组指令(具有相同的名称)但不同的输入值集,可以与 Java 中的重载方法进行比较,如图 3.17 所示。

再次强调,重载方法是定义在同一个类中,名称相同但方法参数列表不同的方法。如图 3.17 所示,重载方法使得添加具有相似功能但与不同输入值集一起工作的方法变得更加容易。

图 3.17. 重载方法的真实示例

让我们以 Java API 类中的一个常用示例为例:System.out.println()println方法接受多种类型的方法参数:

当你使用println方法时,你知道你传递给它作为方法参数的任何内容都将打印到控制台。使用像printlnIntprintlnBoolprintlnString这样的方法来执行相同的功能不是疯狂的吗?我也这么认为。但观点在不同条件下会改变。有时,你可能会使用特定方法而不是重载,因为这样读起来更清晰,避免了混淆。随着你编写更多代码,你将能够自己判断这些情况。

定义重载方法时需要记住的规则

这里有一些定义超载方法的规则:

  • 超载方法必须具有彼此不同的方法参数。

  • 超载方法可能或可能不定义不同的返回类型。

  • 超载方法可能或可能不定义不同的访问级别。

  • 不能仅通过更改返回类型或访问修饰符或两者来定义超载方法。

接下来,我将详细描述前面的规则——有效参数列表、返回类型和访问级别以定义超载方法。

3.4.1. 参数列表

超载方法接受不同的参数列表。参数列表可以在以下任何方面有所不同:

  • 接受的参数数量变化

  • 接受的参数类型变化

  • 接受的参数位置变化(基于参数类型,而不是变量名)

下面是超载方法 calcAverage 的一个示例,它接受不同数量的方法参数:

图片

前面的代码是超载方法最简单形式的示例。你还可以定义参数列表差异在于接受的参数类型的超载方法:

图片

但是,你不能仅仅通过将数组参数转换为可变参数或反之亦然来定义超载方法(除非可变参数或数组项类型保持不变)。在幕后,可变参数被实现为数组。因此,以下超载方法无法编译:

图片

如果它们仅更改传递给它们的参数的位置,则这些方法也是正确超载的:

图片

尽管你可能认为接受的参数完全相同,只是位置不同,但 Java 编译器将它们视为不同的参数列表。编译器可以通过查看你在代码中指定的参数的 序列 来理解你想要调用哪个方法实现。因此,前面的代码是超载方法的有效示例。

但是,当你尝试使用可以传递给超载方法两个版本的值来执行此方法时,会出现问题。在这种情况下,代码将无法编译:

图片

在前面的代码中,图片 定义了接受两个方法参数的方法 calcAverage:一个 double 和一个 int图片 定义了超载方法 calcAverage,它接受两个方法参数:一个 int 和一个 double。因为整型字面量可以传递给 double 类型的变量,所以字面值 23 可以传递给在 图片图片 声明的超载方法中。因为这个方法调用是可疑的,所以 图片 无法编译。

3.4.2. 返回类型

如果方法仅在返回类型上有所不同,则不能将方法定义为超载方法,因为返回类型不是方法签名的一部分:

图片

前面的代码中的方法不能被称为重载方法。

3.4.3. 访问级别

如果方法仅在访问级别上不同,则不能将它们定义为重载方法:

如果你定义了如前所示的重载 calcAverage 方法,代码将无法编译。

在下一节中,你将创建称为构造函数的特殊方法,用于创建类的对象。

3.5. 构造函数

[6.3] 创建和重载构造函数;包括对默认构造函数的影响

在本节中,你将创建构造函数,了解默认构造函数和用户定义构造函数之间的区别,并创建重载的构造函数。

当你开设一个新的银行账户时会发生什么?根据你银行提供的服务,你可能会被分配一个新的银行账户号码,提供支票簿,并允许访问银行为你创建的新在线账户。这些详细信息是在设置你的新银行账户时创建并返回给你的。

将这些步骤与 Java 中构造函数的行为进行比较,如图 3.18 所示。

图 3.18. 创建新银行账户时可能执行的一系列步骤。这些步骤可以与 Java 中构造函数的行为进行比较。

构造函数 是特殊的方法,用于创建并返回它们定义的类中的对象。构造函数与它们定义的类的名称相同,并且它们不指定返回类型——甚至不是 void

构造函数可以完成以下任务:

  • 调用超类的构造函数;这可以是一个隐式或显式调用。

  • 使用默认值初始化类的所有实例变量。

构造函数有两种类型:用户定义的构造函数和默认构造函数,我们将在下一节中详细讨论。

3.5.1. 用户定义的构造函数

类的作者对类的定义有完全的控制权。作者可以在类中定义构造函数,也可以不定义。如果作者在类中定义了构造函数,它被称为 用户定义的构造函数。这里的词 用户 并不指另一个使用这个类的人或类,而是指创建这个类的人。它被称为“用户定义的”,因为它不是由 Java 编译器创建的。

图 3.19 展示了一个名为 Employee 的类,它定义了一个构造函数。

图 3.19. 用户 Paul 定义的具有构造函数的类 Employee

这是一个创建 Employee 类对象的 Office 类:

在前面的例子中, 使用关键字 new 创建了 Employee 类的对象,这触发了 Employee 类构造函数的执行。Office 类的输出如下:

Constructor

因为构造函数在创建对象时立即被调用,你可以用它来为类的实例变量分配默认值,如下所示(修改和额外的代码用粗体标出):

让我们在Office类中创建一个Employee类的对象,看看是否有任何区别:

前面代码的输出如下:

Constructor
20

因为构造函数是一个方法,你也可以向它传递方法参数,如下所示(更改的地方用粗体标出):

class Employee {
    String name;
    int age;
    Employee(int newAge, String newName) {
        name = newName;
        age = newAge;
        System.out.println("Constructor");
    }
}

你可以通过传递所需的方法参数来在Office类中使用这个构造函数,如下所示:

class Office {
    public static void main(String args[]) {
        Employee emp = new Employee(30, "Pavni Gupta");
    }
}

回顾之前提到的构造函数的使用和声明。请注意,当你创建一个类的对象时,会调用构造函数。构造函数确实有一个隐式的返回类型,即它所定义的类。它创建并返回其类的对象,这就是为什么你不能为构造函数定义返回类型。此外,请注意,你可以使用四个访问级别中的任何一个来定义构造函数。

考试技巧

你可以使用所有四个访问级别来定义构造函数:publicprotected、默认和private

如果你为构造函数定义一个返回类型会发生什么?Java 会将其视为另一个方法,而不是构造函数,这也意味着当你创建其类的对象时,它不会隐式调用:

在前面的例子中,不会调用在Employee类中定义的返回类型为void的方法Employee。因为方法Employee将其返回类型定义为void,它不再被视为构造函数。

如果Employee类将Employee方法的返回类型定义为void,Java 如何使用它来创建对象?具有返回类型void的方法在Employee类中被视为任何其他方法。这个逻辑适用于所有其他数据类型:如果你将构造函数的返回类型定义为任何数据类型——例如charintStringlongdouble或任何其他类——它将不再被视为构造函数。

如何执行这样的方法?通过显式调用它,如下面的代码所示(修改的代码用粗体标出):

注意,前面代码中的Employee方法就像在Employee类中定义的任何其他方法一样被调用。当你创建Employee类的对象时,它不会自动被调用。正如前面代码所示,在具有相同名称的类中,允许定义不是构造函数的方法。很有趣。

但请注意,OCA 考试的作者也发现了这一点很有趣,你可能会遇到一些关于这个概念棘手的问题。别担心:有了正确的信息,你肯定能正确回答它们。

考试技巧

构造函数不得定义任何返回类型。相反,它创建并返回其定义的类的对象。如果您为构造函数定义了返回类型,它将不再被视为构造函数。相反,它将被视为一个普通方法,即使它与它的类具有相同的名称。

初始化器块与构造函数

初始化器块是在类中定义的,而不是作为方法的一部分。它为每个创建的类的对象执行。在以下示例中,类Employee定义了一个初始化器块:

![181fig02_alt.jpg]

在以下代码中,类TestEmp创建了一个Employee类的对象:

![182fig01_alt.jpg]

如果您为类定义了初始化器和构造函数,这两个都会执行。初始化器块将在构造函数之前执行:

![182fig02_alt.jpg]

TestEmp的输出如下:

Employee:initializer
Employee:constructor

如果一个类定义了多个初始化器块,它们的执行顺序取决于它们在类中的位置。但它们都在类的构造函数之前执行:

![182fig03_alt.jpg]

以下是前面代码的输出:

Employee:initializer 1
Employee:initializer 2
Employee:constructor

前面的代码示例让您疑惑为什么需要同时有一个初始化器块和一个构造函数,如果它们都在对象的创建时执行?初始化器块用于初始化匿名类的变量。匿名类是一种内部类。在没有名称的情况下,匿名类无法定义构造函数,并依赖于初始化器块在创建其类的对象时初始化其变量。由于内部类不在此考试范围内,我将不讨论如何使用初始化器块与匿名内部类一起使用。

初始化器块内可以发生很多操作:它可以创建局部变量。它可以访问并分配实例和静态变量的值。它可以调用方法并定义循环、条件语句和try-catch-finally块。与构造函数不同,初始化器块不能接受方法参数。

注意

循环和条件语句在第五章中介绍,而try-catch-finally块在第七章中介绍。

3.5.2. 默认构造函数

在前面关于用户定义构造函数的章节中,我讨论了构造函数用于创建对象的方式。如果您在类中没有定义任何构造函数会发生什么?

以下代码是未定义构造函数的Employee类的示例:

![183fig01.jpg]

您可以在另一个类(Office)中创建此类的对象,如下所示:

![183fig02_alt.jpg]

在这种情况下,哪个方法创建了Employee类的对象?图 3.20 显示了当编译一个没有定义任何构造函数的类(Employee)时会发生什么。在没有用户定义的构造函数的情况下,Java 插入一个默认构造函数。这个构造函数不接受任何方法参数。它调用超类(父类)的构造函数并将所有实例变量赋值为默认值。

图 3.20。当 Java 编译器编译一个没有定义构造函数的类时,编译器会为其创建一个。

图片

考试技巧

默认构造函数的可访问性与其类的可访问性相匹配。Java 为公共类创建一个公共默认构造函数。它为具有包级访问权限的类创建一个具有包访问权限的默认构造函数。

如果你向Employee类添加另一个构造函数,如下例所示,会发生什么?

图片

在这种情况下,在重新编译时,Java 编译器会注意到你在Employee类中定义了一个构造函数。它不会为其添加默认构造函数,如图 3.21 所示。

图 3.21。当一个带有构造函数的类被编译时,Java 编译器不会为其添加默认构造函数。

图片

如果没有无参构造函数,以下代码将无法编译:

图片

考试技巧

Java 只在没有定义构造函数的情况下定义默认构造函数。如果一个类没有定义构造函数,编译器会为该类添加一个默认的无参构造函数。但是,如果你后来通过向类中添加构造函数来修改类,Java 编译器将移除它最初添加到类中的默认无参构造函数。

3.5.3. 重载构造函数

就像你可以在类中重载方法一样,你也可以在类中重载构造函数。重载构造函数遵循与上一节中讨论的重载方法相同的规则。以下是一个快速回顾:

  • 重载构造函数必须使用不同的参数列表来定义。

  • 重载构造函数不能仅通过改变访问级别来定义。

因为构造函数没有定义返回类型,所以定义具有不同返回类型的不合法重载构造函数是没有意义的。

以下是一个定义了四个重载构造函数的Employee类的示例:

图片

在前面的代码中,图片定义了一个不接受任何方法参数的构造函数。图片定义了另一个接受单个方法参数的构造函数。注意图片图片中定义的构造函数。这两个都接受两个方法参数,Stringint。但是,这两个方法参数在图片图片中的位置不同,这在重载构造函数和方法中是可接受且有效的。

从另一个构造函数调用重载的构造函数

在一个类中定义多个构造函数并跨构造函数重用其功能是很常见的。与可以通过方法名调用的重载方法不同,重载构造函数是通过使用关键字 this 调用的——这是一个对所有引用对象本身的隐式引用,对所有对象都是可访问的:

的代码创建了一个无参数的构造函数。在 ,这个构造函数通过传递 null0 的值来调用重载的构造函数。 定义了一个接受两个方法参数的重载构造函数。

因为构造函数是使用其类的名称定义的,所以尝试使用类的名称从另一个构造函数调用构造函数是一个常见的错误:

此外,当你使用关键字 this 调用一个重载构造函数时,它必须是你的构造函数中的第一个语句:

你不能在构造函数内部调用两个(或更多)构造函数,因为构造函数的调用必须是构造函数中的第一个语句:

不仅如此:你也不能从你的类中的任何其他方法调用构造函数。类 Employee 的其他方法都不能调用其构造函数。

需要记住的规则

这里是一个快速列表,用于记住考试中定义和使用重载构造函数的规则:

  • 重载构造函数必须使用不同的参数列表定义。

  • 重载构造函数不能仅通过改变访问级别来定义。

  • 重载构造函数可以使用不同的访问级别定义。

  • 构造函数可以通过使用关键字 this 来调用另一个重载的构造函数。

  • 构造函数不能使用其类的名称来调用构造函数。

  • 如果存在,对另一个构造函数的调用必须是构造函数中的第一个语句。

  • 你不能从构造函数中调用多个构造函数。

  • 构造函数不能从方法中调用(除了使用 new 关键字实例化类)。

故事中的下一个转折点练习在其代码中隐藏了一个重要的概念,只有当你执行修改后的代码时才能了解(答案见附录)。

故事转折点 3.2

让我们修改我在重载构造函数部分使用的类 Employee 的定义,如下所示:

class Employee {
    String name;
    int age;
    Employee() {
        this ();
    }
    Employee (String newName, int newAge) {
        name = newName;
        age = newAge;
    }
}

问题:这段修改后的代码的输出是什么,为什么?

现在你已经看到了如何创建方法和构造函数,以及它们的重载变体,我们将在下一节中转向如何使用所有这些来访问和修改对象字段。

3.6. 访问对象字段

[2.3] 了解如何读取或写入对象字段

在本节中,你将学习什么是对象字段以及如何读取、初始化和修改它们。你还将学习用于在对象上调用方法的正确符号。访问修饰符还决定了你是否可以在对象上调用方法。

3.6.1. 什么是对象字段?

对象字段是类中定义的实例变量的另一个名称。我经常看到一些认证考生对对象字段是否与类的实例变量相同感到困惑。

下面是一个Star类的示例:

在前面的例子中,定义了一个实例变量,starAge定义了一个setter方法,setAgesetter(或mutator)方法用于设置变量的值。定义了一个getter(或accessor)方法,getAgegetter方法用于检索变量的值。在这个例子中,对象字段是starAge,而不是agenewAge。对象字段的名字不由其gettersetter方法的名字决定。

JavaBeans 属性和对象字段

对象字段名称的混淆原因在于 Java 类也可以用来定义称为JavaBeans的视觉或非视觉组件,这些组件用于 Spring、Hibernate 和其他视觉和非视觉环境中。这些类应该定义用于检索和设置视觉组件属性的 getter 和 setter 方法。如果一个视觉 JavaBean 组件定义了一个名为age的属性,那么它的 getter 和 setter 方法的名字将是getAgesetAge。对于 JavaBean,你不必担心用于存储此属性值的变量的名称。在 JavaBean 中,一个对象字段thisIsMyAge-可以用来存储其属性age的值。

注意,我提到的 JavaBeans 不是企业 JavaBeans。企业 JavaBeans 用于用 Java 编写的企业应用程序,这些应用程序在服务器上运行。

3.6.2. 读取和写入对象字段

OCA Java SE 8 程序员 I 级考试将测试你如何从对象字段中读取值并将它们写入,这可以通过以下任何一种方式完成:

  • 使用方法来读取和写入对象字段

  • 使用构造函数将值写入对象字段

  • 直接访问实例变量以读取和写入对象字段

考试技巧

虽然可以通过直接访问来操作对象字段,但这并不是推荐的做法。这样做会使对象容易受到无效数据的影响。这样的类并没有很好地封装。

此考试目标(2.3)还将测试你对如何为多个对象分配相同对象字段的不同值的理解。让我们从一个例子开始:

在类Employee中, 定义了两个对象字段:nameage。它定义了一个(无参数)构造函数。 将值22分配给其字段age。此类还定义了一个setName方法,其中 将传递给它的值分配给对象字段nameprintEmp方法用于打印对象字段nameage的值。

以下是一个类的定义,Office,它创建了两个Employee类的实例,e1e2,并为其字段分配值。让我们看看类Office的定义:

class Office {
    public static void main(String args[]) {
        Employee e1 = new Employee();
        Employee e2 = new Employee();
        e1.name = "Selvan";
        e2.setName("Harry");
        e1.printEmp();
        e2.printEmp();
    }
}

这是前面代码的输出:

name = Selvan age = 22
name = Harry age = 22

图 3.22 定义了对象图(包含对象的名字和类型、对象字段的名称及其对应值),这将有助于您更好地理解前面的输出。

图 3.22. 类Employee的两个对象

您可以通过使用其变量name或使用setName方法来访问类Employee的对象字段名称。以下行代码将值Selvan分配给对象e1的字段name

e1.name = "Selvan";

以下行代码使用setName方法将值Harry分配给对象e2的字段name

e2.setName("Harry");

因为类Employee的构造函数将值22分配给变量age,所以对象e1e2都包含相同的值,22

如果您没有为对象字段分配任何值并尝试打印其值会发生什么?如果在写入任何值之前尝试访问或读取它们的值,所有实例变量(对象字段)都将分配它们的默认值:

前面代码的输出如下(对象的默认值为nullint的默认值为0):

name = null age = 0

如果您将变量name的访问修饰符更改为private,如下所示(加粗的修改后的代码),会发生什么?

您无法将对象字段名称的值设置为以下形式:

e1.name = "Selvan";

这行代码无法编译。相反,它抱怨变量name在类Employee中具有私有访问权限,并且不能从任何其他类中访问:

Office.java:6:  name has private access in Employee
            e1.name = "Selvan";

当你在考试中回答从对象字段读取值和写入值的问题时,请注意以下要点:

  • 对象字段的访问修饰符

  • 用于读取和写入对象字段值的方法的访问修饰符

  • 将值分配给对象字段的构造函数

3.6.3. 在对象上调用方法

您可以使用对象引用变量调用类中定义的方法。在本考试目标中,此考试将特别测试以下内容:

  • 调用一个对象引用变量上的方法的正确表示法

  • 必须传递给方法的方法参数的正确数量

  • 被分配给变量的方法的返回值

Java 使用点符号(.)在引用变量上执行方法。假设Employee类定义如下:

你可以创建一个Employee类的对象,并像这样调用其上的setName方法:

Employee e1 = new Employee();
e1.setName("Java");

以下方法调用在 Java 中是无效的:

当你调用一个方法时,你必须传递给它由它定义的确切数量的方法参数。在Employee类的先前定义中,setName方法定义了一个类型为String的方法参数。你可以将字面值或变量传递给方法,作为方法参数。以下代码调用是正确的:

考试提示

调用一个方法后必须传递值给所有它的方法参数。对于一个定义了一个或多个方法参数的方法,你不能在调用方法后跟()来表示它不需要传递值。

如果被调用方法的参数列表在最右侧定义了一个可变参数,你可以使用可变数量的参数来调用该方法。让我们在Employee类中添加一个名为daysOffWork的方法,它接受一个可变数量的参数(以下为粗体修改):

class Employee {
    private String name;
    public void setName(String val) {
        name = val;
    }
    public int daysOffWork(int... days) {
        int daysOff = 0;
        for (int i = 0; i < days.length; i++)
            daysOff += days[i];
        return daysOff;
    }

你可以使用一个可变数量的参数来调用此方法:

上述代码的输出如下:

10
6
考试提示

接受可变参数的方法可以用不同数量的实际参数来调用。此外,接受可变参数的方法可以用数组代替可变参数来调用。

让我们在Employee类中添加一个名为getName的方法,它返回一个String值(以下为粗体修改):

class Employee {
    private String name;
    public void setName(String val) {
        name = val;
    }

    public String getName() {
        return name;
    }
}

你可以将getName方法返回的String值赋给一个String变量或传递给另一个方法,如下所示:

在前面的代码中,setName方法的返回类型是void;因此,你不能用它来给变量赋值:

此外,你不能将一个方法返回的不兼容值赋给一个变量,如下所示:

你可以通过使用方法或直接访问类的实例变量来读取和写入对象字段。但是,允许类外部访问实例变量不是一个好主意。

在下一节中,你将看到将实例变量暴露在类外部的风险以及一个封装良好的类的益处。

3.7. 将封装原则应用于一个类

[6.5] 将封装原则应用于一个类

如本节标题所示,我们将把封装原则应用于一个类。一个封装良好的对象不会将其内部部分暴露给外部世界。它定义了一组方法,使用户能够与之交互。

作为现实世界的一个例子,你可以将一家银行比作一个封装良好的类。银行不会将其内部部分——例如,其保险库和银行账户——暴露给外界,就像 Java 中封装良好的类不应该将其用于存储对象状态的变量暴露给该对象之外。银行定义一系列程序(如对保险库的密钥访问和提款前的验证)来保护其内部部分的方式,与封装良好的类定义方法来访问其变量的方式非常相似。

3.7.1. 封装的需求

类的私有成员——其变量和方法——用于隐藏有关类的信息。你为什么需要隐藏类信息呢?将一个类与你自己比较。你希望其他人知道你所有的弱点吗?你希望其他人能够控制你的思想吗?这同样适用于你用 Java 定义的类。一个类可能需要一些变量和方法来存储对象的状态并定义其行为。但它不希望其他所有类都知道这一点。以下是一个快速列表,说明了为什么要封装 Java 对象的状态:

  • 为了防止外部对象执行危险操作

  • 为了隐藏实现细节,以便实现可以在不影响其他对象的情况下进行第二次更改

  • 为了最小化耦合的可能性

让我们用一个例子来工作。以下是Phone类的定义:

图片

因为变量weight没有被定义为private成员,任何其他类(在同一包中)都可以访问它并向其写入任何值,如下所示:

图片

3.7.2. 应用封装

在上一节中,你可能已经注意到,封装不良的类的对象字段被暴露在类之外。这种方法使用户能够为对象字段分配任意值。

这是否应该被允许?例如,回到 3.7.1 节中讨论的Phone类示例,一部电话的重量如何是负值?

让我们通过在Phone类中将变量weight定义为private变量来解决这个问题,如下所示(省略了无关更改):

class Phone {
    private double weight;
}

但现在这个变量在Home类中将不可访问。让我们定义使用这个变量的方法,这些方法可以在Phone类之外访问(加粗处有变化):

图片

如果方法setWeight传递的参数是一个负值或大于 1,000 的值,则它不会将这个值分配给实例变量weight。这种行为被称为使用公共方法公开对象功能

让我们看看这个方法是如何在Home类中用于将值分配给变量weight的:

图片

注意,当类Home尝试将变量的值设置为-12.2377712.23(超出范围的值)时,这些值不会被分配给Phone的私有变量weight。它接受12.23这个值,它位于定义的范围内。

在 OCA Java SE 8 程序员 I 级考试中,你也可能找到术语“信息隐藏”。封装是在类中定义变量和方法的概念。信息隐藏起源于封装概念的应用和目的。这些术语也可以互换使用。

考试技巧

术语“封装”和“信息隐藏”可以互换使用。通过仅通过方法公开对象功能,您可以防止您的私有变量被分配任何不符合您要求的价值。创建一个良好封装的类的最佳方法之一是将其实例变量定义为私有变量,并允许通过公共方法访问这些变量。

在下一个“故事转折”练习中,有一个关于确定正确封装的类的隐藏技巧。让我们看看你是否能找到它(答案见附录)。

故事转折 3.3

让我们修改我在本节中用来演示封装原则的类Phone的定义。给定以下Phone类的定义,以下哪个选项,当替换第 1-3 行的代码时,使其成为一个良好封装的类?

class Phone {
    public String model;
    double weight;                                   //LINE1
    public void setWeight(double w) {weight = w;}    //LINE2
    public double getWeight() {return weight;}       //LINE3
}

选项:

  1. public double weight;
    private void setWeight(double w) { weight = w; }
    private double getWeight() {    return weight; }
    
  2. b

    public double weight;
    void setWeight(double w) { weight = w; }
    double getWeight() {    return weight; }
    
  3. c

    public double weight;
    protected void setWeight(double w) { weight = w; }
    protected double getWeight() {    return weight; }
    
  4. d

    public double weight;
    public void setWeight(double w) { weight = w; }
    public double getWeight() {    return weight; }
    
  5. 以上皆非

良好封装的类不会在其类外部公开其实例变量。当这些类的这些方法修改传递给它们的参数的状态时,会发生什么?这是否是可接受的行为?我将在下一节中讨论会发生什么。

3.8. 将对象和原始数据类型传递给方法

[6.6] 确定当对象引用和原始数据类型被传递到会改变其值的方法时,它们的影响。

在本节中,你将学习将对象引用和原始数据类型传递给方法的区别。你将确定当它们被传递到会改变其值的方法中时,对对象引用和原始数据值的影响。

当对象引用和原始数据类型被传递到方法中时,由于 Java 内部存储这两种数据类型的方式不同,它们的行为方式也不同。让我们从将原始数据类型传递给方法开始。

3.8.1. 将原始数据类型传递给方法

原始数据类型的值被复制并传递给方法。因此,被复制的值的变量不会改变:

上述代码的输出如下:

0
1
0
注意

在前面的代码中,方法 modifyVal 看似 接受并修改了传递给它的参数。本书包含此类代码,因为你在考试中可能会看到类似的代码,这些代码不遵循编码或命名约定。但请在实际项目中编写代码时遵循编码约定。

方法 modifyVal 接受一个类型为 int 的方法参数 a。在这个方法中,变量 a 是方法参数,它持有传递给它的值的副本。方法增加方法参数 a 的值并打印其值。

当类 Office 调用方法 modifyVal 时,它传递了对象字段 age 的值的一个副本给它。方法 modifyVal 从不访问对象字段 age。因此,在执行此方法之后,方法字段 age 的值再次打印为 0

如果将类 Employee 的定义修改如下(加粗部分为修改)会发生什么:

class Employee {
    int age;
    void modifyVal(int age) {
        age = age + 1;

        System.out.println(age);
    }
}

Office 仍然会打印相同的答案,因为方法 modifyVal 定义了一个名为 age 的方法参数(你还记得本章前面讨论的变量作用域的话题吗?)。请注意以下与将方法参数传递给方法相关的重要点:

  • 定义一个与方法参数同名的方法参数(或对象字段)是可以的。但这不是推荐的做法。

  • 在一个方法中,方法参数优先于对象字段。当方法 modifyVal 指向变量 age 时,它指的是方法参数 age,而不是实例变量 age。要在方法 modifyVal 中访问实例变量 age,变量名 age 需要加上关键字 thisthis 是一个指向对象本身的关键字)。

关键字 this 在 第六章 中有详细讨论。

考试技巧

当你将原始变量传递给一个方法时,该变量在方法执行后其值保持不变。无论方法是否将原始值重新分配给另一个变量或修改它,值都不会改变。

3.8.2. 将对象引用传递给方法

有两种主要情况:

  • 当方法重新分配传递给它的对象引用到另一个变量时

  • 当方法修改传递给它的对象引用的状态时

当方法重新分配传递给它们的对象引用时

当你将对象引用传递给一个方法时,该方法可以将其分配给另一个变量。在这种情况下,传递给方法的对象的状态保持不变。当一个方法传递一个引用值时,传递给被调用方法的引用(即内存地址)的一个副本。被调用者可以对其副本做任何操作,而不会改变调用者持有的原始引用。

以下代码示例解释了这一概念。假设你有以下 Person 类的定义:

class Person {
    private String name;
    Person(String newName) {
        name = newName;
    }
    public String getName() {
        return name;
    }

    public void setName(String val) {
        name = val;
    }
}

你认为以下代码的输出是什么?

在前面的代码中, 创建了两个对象引用,person1person2,这在图 3.23 的第 1 步中得到了说明。方框中的值代表 Person 类的对象。 打印出 John:Paul——person1.nameperson2.name 的值。

图 3.23. Person 类的对象,由变量 person1person2p1p2 引用

然后代码调用 swap 方法,并将 person1person2 所引用的对象传递给它。当这些对象作为参数传递给 swap 方法时,方法参数 p1p2 也引用了这些对象。这种行为在图 3.23 的第 2 步中得到了说明。

swap 方法定义了三行代码:

  • Person temp = p1: 使得 temp 指向 p1 所引用的对象

  • p1 = p: 使得 p1 指向 p2 所引用的对象

  • p2 = temp: 使得 p2 指向 temp 所引用的对象

这三个步骤在图 3.24 中有所展示。

图 3.24. 在 swap 方法执行过程中变量所引用的对象的变化

如图 3.24 所示,引用变量 person1person2 仍然引用它们传递给 swap 方法的对象。因为 person1person2 所引用的对象的值没有发生变化,所以上一页的行号 再次打印出 John:Paul

前一段代码的输出如下:

John:Paul
John:Paul
当方法修改传递给它们的对象引用的状态时

让我们看看一个方法如何改变对象的内部状态,使得修改后的状态可以在调用方法中访问。假设 Person 类的定义与之前相同,这里再次列出以方便您阅读:

class Person {
    private String name;
    Person(String newName) {
        name = newName;
    }
    public String getName() {
        return name;
    }
    public void setName(String val) {
        name = val;
    }
}

以下代码的输出是什么?

前一段代码的输出如下:

John
Rodrigue

resetValueOfMemberVariable 方法接受 person1 所引用的对象,并将其赋值给方法参数 p1。现在这两个变量,person1p1,都引用了同一个对象。p1.setName("Rodrigue") 修改了 p1 所引用的对象的值。因为 person1 也引用了同一个对象,所以 person1.getName()main 方法中返回新的名字,Rodrigue。这一系列操作在图 3.25 中得到了展示。

图 3.25. 将对象状态传递给方法 resetValueOfMember-Variable 的修改

3.9. 概述

我在本章开头讨论了这些变量的作用域:局部变量、方法参数、实例变量和类变量。通常这些变量的作用域会相互重叠。

我还介绍了类的构造函数:用户定义的构造函数和默认构造函数。Java 在没有定义任何构造函数的类中插入一个默认构造函数。你可以修改此类源代码,添加一个构造函数,并重新编译该类。重新编译后,Java 编译器会移除自动生成的构造函数。

我接着介绍了从对象字段读取和写入的子目标。术语 对象字段实例变量 意义相同,可以互换使用。你可以通过直接访问或使用访问器方法来读取和写入对象字段。我还展示了如何将封装原则应用于类,并解释了这样做为什么有用。

最后,我解释了当它们传递给会改变其值的方法时,引用和原始数据类型的影响。当你将原始值传递给方法时,其值永远不会在调用方法中改变。当你将对象引用变量传递给方法时,其值的变化可能会在调用方法中反映出来——如果被调用方法修改了传递给它的对象字段。如果被调用方法在修改其字段值之前将其方法参数赋值为新的对象引用,则这些更改在调用方法中是不可见的。

3.10. 复习笔记

本节列出了本章涵盖的主要要点。

变量的作用域:

  • 变量可以有多个作用域:类、实例、局部和方法参数。

  • 局部变量是在方法内部定义的。循环变量是在定义它们的循环内部局部有效的。

  • 如果局部变量在方法中的子块(在大括号 {} 内)中声明,则其作用域小于方法的作用域。这个子块可以是 if 语句、switch 构造、循环或 try-catch 块(在第七章中讨论)。

  • 局部变量不能在定义它们的函数外部访问。

  • 在方法中,局部变量在其声明之前不能被访问。

  • 实例变量是在对象内部定义和可访问的。它们对类的所有实例方法都是可访问的。

  • 类变量被类中的所有对象共享——即使没有该类的对象,也可以访问它们。

  • 方法参数用于在方法中接受参数。它们的范围仅限于定义它们的那个方法。

  • 不能使用相同的名称定义方法参数和局部变量。

  • 类和实例变量不能使用相同的名称定义。

  • 局部和实例变量可以使用相同的名称定义。在方法中,如果存在与实例变量同名的一个局部变量,则局部变量具有优先权。

对象的生命周期:

  • 对象的生命周期从初始化开始,一直持续到它超出作用域或不再被变量引用为止。

  • 当一个对象存活时,它可以被变量引用,其他类可以通过调用其方法和访问其变量来使用它。

  • 声明一个引用对象变量并不等同于创建一个对象。

  • 对象使用new运算符创建。字符串在编译器中内置了特殊简写。字符串可以使用双引号创建,如"Hello"

  • 当一个对象无法再被访问时,它会被标记为可回收垃圾。

  • 如果一个对象无法再被任何变量引用,它就会变得不可访问,这通常发生在引用变量被显式设置为null或超出作用域时。

  • 垃圾收集器也可以从一组引用对象中回收内存。这组变量被称为孤立岛。

  • 你只能确定对象是否被标记为垃圾回收。你永远不能确定对象是否已被垃圾回收。

创建带有参数和返回值的方法:

  • 方法的返回类型说明了方法将返回的值的类型。

  • 你可以为方法定义多个方法参数。

  • 方法参数可以是原始类型或类的对象或接口的对象。

  • 方法参数由逗号分隔。

  • 与局部变量的声明或实例和类字段不同,每个方法参数都必须在其类型之前。不允许这样做:void description(String name, age) {}

  • 你可以在参数列表中定义一个唯一的可变参数,并且它必须是参数列表中的最后一个变量。如果不遵循这两个规则,你的代码将无法编译。

  • 对于返回值的方法,return语句必须立即跟一个兼容的值。

  • 对于不返回值的方法(返回类型为void),return语句后面不能跟返回值。

  • 如果有代码只能在return语句之后执行,类将无法编译。

  • 方法可以可选地接受方法参数。

  • 方法可以可选地返回一个值。

  • 方法通过使用关键字return后跟变量名来返回值,其值被传递回调用方法。

  • 方法返回的值可能或可能不被分配给变量。如果值被分配给变量,变量类型应与返回值的类型兼容。

  • return语句应该是方法中的最后一个语句。在return语句之后放置的语句是不可访问的,并且无法编译。

  • 一种方法可以接受零个或多个参数,但只能返回零个或一个值。

创建一个覆载方法:

  • 覆载的方法接受不同的参数列表。参数列表可以不同

    • 接受参数数量的变化

    • 接受参数类型的变化

    • 接受参数位置的变化

  • 如果方法仅在返回类型或访问级别上有所不同,则不能将方法定义为重载方法。

类的构造函数:

  • 构造函数是在类中定义的特殊方法,用于创建并返回它们所定义的类的对象。

  • 构造函数与类的名称相同,它们不指定返回类型——甚至不是void

  • 用户定义的构造函数由开发者定义。

  • 如果一个类定义了多个初始化块,它们的执行顺序取决于它们在类中的位置。但它们都在类的构造函数之前执行。

  • 默认构造函数由 Java 定义,但前提是开发者没有在类中定义任何构造函数。

  • 您可以使用四种访问级别定义构造函数:publicprotected、默认和private

  • 默认构造函数的访问性与其类的访问性相匹配。Java 为公共类创建一个公共默认构造函数。它为具有包级访问权限的类创建一个具有包访问权限的默认构造函数。

  • 如果您为构造函数定义了返回类型,它将不再被视为构造函数。它将被视为普通方法,即使它与它的类具有相同的名称。

  • 初始化块是在类中定义的,而不是作为方法的一部分。它为类创建的每个对象执行。

  • 如果您为类定义了初始化块和构造函数,这两个都将执行。初始化块将在构造函数之前执行。

  • 与构造函数不同,初始化块不能接受方法参数。

  • 初始化块可以创建局部变量。它可以访问和分配实例和静态变量的值。它可以调用方法并定义循环、条件语句和try-catch-finally块。

重载构造函数:

  • 一个类也可以定义重载构造函数。

  • 重载构造函数必须使用不同的参数列表来定义。

  • 重载构造函数不能仅通过改变访问级别来定义。

  • 重载构造函数可以使用不同的访问级别来定义。

  • 构造函数可以通过使用关键字this来调用另一个重载构造函数。

  • 构造函数不能通过使用其类名来调用另一个构造函数。

  • 如果存在,对另一个构造函数的调用必须是构造函数中的第一条语句。

访问对象字段:

  • 对象字段是类中定义的实例变量的另一个名称。

  • 对象字段可以通过直接访问变量(如果其访问级别允许)或通过使用返回其值的方法来读取。

  • 虽然可以通过直接访问来操作对象字段,但这并不是推荐的做法。这会使对象容易受到无效数据的影响。这样的类没有很好地封装。

  • 对象字段可以通过直接访问变量(如果其访问级别允许)或通过使用接受值并将其分配给实例变量的构造函数和方法来写入。

  • 你可以使用对象引用变量调用类中定义的方法。

  • 你不能在构造函数内部调用两个(或更多)构造函数,因为构造函数的调用必须是构造函数中的第一条语句。

  • 调用一个方法时,必须传递正确数量和类型的方法参数。

  • 调用一个方法后必须传递所有方法参数的值。对于一个定义了一个或多个方法参数的方法,你不能调用方法后跟 () 来表示它不需要传递值。

  • 接受可变参数的方法可以用不同数量的实际参数来调用。

将封装原则应用于类:

  • 一个封装良好的对象不会将其内部部分暴露在对象外部。它定义了一组定义良好的接口(方法),使用户能够与之交互。

  • 一个没有良好封装的类有风险,其变量可能被类的调用者赋予不希望的价值,这可能导致对象的状态不稳定。

  • 术语“封装”和“信息隐藏”可以互换使用。

  • 要定义一个封装良好的类,将其实例变量定义为私有变量。允许通过方法访问或操作这些变量。

将对象和原始数据类型传递给方法:

  • 当对象和原始数据类型被传递给方法时,由于这两种数据类型在 Java 内部存储方式的不同,它们的行为会有所不同。

  • 当你将原始变量传递给一个方法时,该变量在方法执行后其值保持不变。这不会改变,无论方法是否将原始值重新分配给另一个变量或修改它。

  • 当你将一个对象传递给一个方法时,方法可以通过执行其方法来修改对象的状态。在这种情况下,对象修改后的状态会在调用方法中反映出来。

3.11. 样本考试问题

Q3-1.

哪个选项定义了一个封装良好的类?

  1. class Template {
        public String font;
    }
    
  2. class Template2 {
        public String font;
        public void setFont(String font) {
            this.font = font;
        }
        public String getFont() {
            return font;
        }
    }
    
  3. class Template3 {
        private String font;
        public String author;
        public void setFont(String font) {
            this.font = font;
        }
        public String getFont() {
            return font;
        }
        public void setAuthor(String author) {
            this.author = author;
        }
        public String getAuthor() {
            return author;
        }
    }
    
  4. 以上皆非

Q3-2.

检查以下代码并选择正确的选项(s):

public class Person {
    public int height;
    public void setHeight(int newHeight) {
        if (newHeight <= 300)
            height = newHeight;
    }
}
  1. Personheight 永远不能设置为超过 300。
  2. 上述代码是一个封装良好的类的示例。
  3. 如果高度验证没有被设置为 300,这个类将会有更好的封装性。
  4. 即使类没有良好的封装性,它也可以被其他类继承。

Q3-3.

以下哪个方法正确地接受三个整数作为方法参数,并以浮点数的形式返回它们的和?

  1. public void addNumbers(byte arg1, int arg2, int arg3) {
        double sum = arg1 + arg2 + arg3;
    }
    
  2. public double subtractNumbers(byte arg1, int arg2, int arg3) {
        double sum = arg1 + arg2 + arg3;
        return sum;
    }
    
  3. public double numbers(long arg1, byte arg2, double arg3) {
        return arg1 + arg2 + arg3;
    }
    
  4. public float wakaWakaAfrica(long a1, long a2, short a977) {
        double sum = a1 + a2 + a977;
        return (float)sum;
    }
    

Q3-4.

以下哪个陈述是正确的?

  1. 如果方法的返回类型是 int,则方法可以返回 byte 类型的值。

  2. 一个方法可能返回也可能不返回值。

  3. 如果方法的返回类型是 void,它可以定义一个不带值的 return 语句,如下所示:

  4. return;
    
  5. 一个方法可能接受也可能不接受任何方法参数。

  6. 一个方法必须接受至少一个方法参数或定义其返回类型。

  7. 返回类型为String的方法不能返回null

Q3-5.

给定以下Person类的定义,

class Person {
    public String name;
    public int height;
}

以下代码的输出是什么?

class EJavaGuruPassObjects1 {
    public static void main(String args[]) {
        Person p = new Person();
        p.name = "EJava";
        anotherMethod(p);
        System.out.println(p.name);
        someMethod(p);
        System.out.println(p.name);
    }

    static void someMethod(Person p) {
        p.name = "someMethod";
        System.out.println(p.name);
    }
    static void anotherMethod(Person p) {
        p = new Person();
        p.name = "anotherMethod";
        System.out.println(p.name);
    }
}
  1. anotherMethod
    anotherMethod
    someMethod
    someMethod
    
  2. anotherMethod
    EJava
    someMethod
    someMethod
    
  3. anotherMethod
    EJava
    someMethod
    EJava
    
  4. 编译错误

Q3-6.

以下代码的输出是什么?

class EJavaGuruPassPrim {
    public static void main(String args[]) {
        int ejg = 10;
        anotherMethod(ejg);
        System.out.println(ejg);
        someMethod(ejg);
        System.out.println(ejg);
    }
    static void someMethod(int val) {
        ++val;
        System.out.println(val);
    }
    static void anotherMethod(int val) {
        val = 20;
        System.out.println(val);
    }
}
  1. 20
    10
    11
    11
    
  2. 20
    20
    11
    10
    
  3. 20
    10
    11
    10
    
  4. 编译错误

Q3-7.

给定以下eJava方法的签名,选择正确重载此方法的选项:

public String eJava(int age, String name, double duration)
  1. private String eJava(int val, String firstName, double dur)
  2. public void eJava(int val1, String val2, double val3)
  3. String eJava(String name, int age, double duration)
  4. float eJava(double name, String age, byte duration)
  5. ArrayList<String> eJava()
  6. char[] eJava(double numbers)
  7. String eJava()

Q3-8.

给定以下代码,

class Course {
    void enroll(long duration) {
        System.out.println("long");
    }
    void enroll(int duration) {
        System.out.println("int");
    }
    void enroll(String s) {
        System.out.println("String");
    }
    void enroll(Object o) {
        System.out.println("Object");
    }
}

以下代码的输出是什么?

class EJavaGuru {
    public static void main(String args[]) {
        Course course = new Course();
        char c = 10;
        course.enroll(c);
        course.enroll("Object");
    }
}
  1. 编译错误
  2. 运行时异常
  3. int
    String
    
  4. long
    Object
    

Q3-9.

检查以下代码并选择正确的选项:

class EJava {
    public EJava() {
        this(7);
        System.out.println("public");
    }
    private EJava(int val) {
        this("Sunday");
        System.out.println("private");
    }
    protected EJava(String val) {
        System.out.println("protected");
    }
}
class TestEJava {
    public static void main(String[] args) {
        EJava eJava = new EJava();
    }
}
  1. EJava类定义了三个重载的构造函数。

  2. EJava类定义了两个重载的构造函数。私有构造函数不计入重载构造函数。

  3. 不同访问修饰符的构造函数不能相互调用。

  4. 代码打印以下内容:

  5. protected
    private
    public
    
  6. 代码打印以下内容:

  7. public
    private
    protected
    

Q3-10.

选择错误的选项:

  1. 如果用户为public类定义了一个private构造函数,Java 将为该类创建一个public的默认构造函数。

  2. 获取默认构造函数的类没有重载构造函数。

  3. 用户可以重载类的默认构造函数。

  4. 以下类有资格获得默认构造函数:

  5. class EJava {}
    
  6. 以下类也有资格获得默认构造函数:

  7. class EJava {
            void EJava() {}
    }
    

3.12. 样本考试题目的答案

Q3-1.

哪个选项定义了一个封装良好的类?

  1. class Template {
        public String font;
    }
    
  2. class Template2 {
        public String font;
        public void setFont(String font) {
            this.font = font;
        }
        public String getFont() {
            return font;
        }
    }
    
  3. class Template3 {
        private String font;
        public String author;
        public void setFont(String font) {
            this.font = font;
        }
        public String getFont() {
            return font;
        }
        public void setAuthor(String author) {
            this.author = author;
        }
        public String getAuthor() {
            return author;
        }
    }
    
  4. 以上皆非

答案:d

说明:选项(a)、(b)和(c)都是错误的,因为它们都定义了一个公共实例变量。一个封装良好的类应该像一个胶囊,将其实例变量隐藏在外部世界之外。您应该通过类的公共方法访问和修改实例变量,以确保外部世界只能访问类允许它访问的变量。通过定义方法为其实例变量赋值,一个类可以控制可以分配给它们的值的范围。

Q3-2.

检查以下代码并选择正确的选项:

public class Person {
    public int height;
    public void setHeight(int newHeight) {
        if (newHeight <= 300)
            height = newHeight;
    }
}
  1. Personheight永远不会被设置为超过 300。
  2. 上述代码是一个封装良好的类的示例。
  3. 如果高度验证不是设置为 300,类将具有更好的封装性。
  4. 即使类没有良好的封装性,它也可以被其他类继承。

答案:d

解释:这个类没有很好地封装,因为它的实例变量 height 被定义为 public 成员。由于实例变量可以直接被其他类访问,变量并不总是使用 setHeight 方法来设置其 height。类 Person 无法控制可以分配给其公共变量 height 的值。

Q3-3.

以下哪个方法正确地接受三个整数作为方法参数,并返回它们的和作为浮点数?

  1. public void addNumbers(byte arg1, int arg2, int arg3) {
        double sum = arg1 + arg2 + arg3;
    }
    
  2. public double subtractNumbers(byte arg1, int arg2, int arg3) {
        double sum = arg1 + arg2 + arg3;
        return sum;
    }
    
  3. public double numbers(long arg1, byte arg2, double arg3) {
        return arg1 + arg2 + arg3;
    }
    
  4. public float wakaWakaAfrica(long a1, long a2, short a977) {
        double sum = a1 + a2 + a977;
        return (float)sum;
    }
    
  5. 答案:b, d 解释:选项 (a) 是错误的。问题指定该方法应返回一个十进制数(类型 doublefloat),但此方法没有返回任何值。选项 (b) 是正确的。此方法接受三个整数值,这些值可以自动转换为整数:byteintint。它计算这些整数值的总和,并将其作为十进制数(数据类型 double)返回。请注意,方法的名称是 subtractNumbers,这并不使它成为一个无效选项。实际上,如果你在添加它们,你不会将方法命名为 subtractNumbers。但从语法和技术上讲,此选项符合问题的要求,是一个正确的选项。选项 (c) 是错误的。此方法不接受整数作为方法参数。方法参数 arg3 的类型是 double,这不是整数。选项 (d) 是正确的。尽管方法的名称看起来很奇怪,但它接受正确的参数列表(所有整数)并以正确的数据类型(float)返回结果。

Q3-4.

以下哪个陈述是正确的?

  1. 如果方法的返回类型是 int,则方法可以返回 byte 类型的值。

  2. 方法可能返回也可能不返回值。

  3. 如果方法的返回类型是 void,则可以定义一个没有值的 return 语句,如下所示:

  4. return;
    
  5. 方法可能接受也可能不接受任何方法参数。

  6. 方法应至少接受一个方法参数或定义其返回类型。

  7. 返回类型为 String 的方法不能返回 null

答案:a, b, c, d

解释:选项 (e) 是错误的。无论方法是否返回值,对传递给方法参数的数量没有限制。

选项 (f) 是错误的。对于返回原始数据类型的方法,你不能返回 null 值。对于返回对象的方法(String 是一个类,而不是原始数据类型),你可以返回 null

Q3-5.

给定以下 Person 类的定义,

class Person {
    public String name;
    public int height;
}

以下代码的输出是什么?

class EJavaGuruPassObjects1 {
    public static void main(String args[]) {
        Person p = new Person();
        p.name = "EJava";

        anotherMethod(p);
        System.out.println(p.name);
        someMethod(p);
        System.out.println(p.name);
    }
    static void someMethod(Person p) {
        p.name = "someMethod";
        System.out.println(p.name);
    }
    static void anotherMethod(Person p) {
        p = new Person();
        p.name = "anotherMethod";
        System.out.println(p.name);
    }
}
  1. anotherMethod
    anotherMethod
    someMethod
    someMethod
    
  2. anotherMethod
    EJava
    someMethod
    someMethod
    
  3. anotherMethod
    EJava
    someMethod
    EJava
    
  4. 编译错误

答案:b

说明:类 EJavaGuruPassObject1 定义了两个方法,someMethodanotherMethod。方法 someMethod 修改了传递给它的对象参数的值。因此,这些更改在方法内部和调用方法(main 方法)中都是可见的。但方法 anotherMethod 重新分配了传递给它的引用变量。对此对象任何值的更改仅限于此方法。它们不会反映在调用方法(main 方法)中。

Q3-6.

以下代码的输出是什么?

class EJavaGuruPassPrim {
    public static void main(String args[]) {
        int ejg = 10;
        anotherMethod(ejg);
        System.out.println(ejg);
        someMethod(ejg);
        System.out.println(ejg);
    }

    static void someMethod(int val) {
        ++val;
        System.out.println(val);
    }
    static void anotherMethod(int val) {
        val = 20;
        System.out.println(val);
    }
}
  1. 20
    10
    11
    11
    
  2. 20
    20
    11
    10
    
  3. 20
    10
    11
    10
    
  4. 编译错误

答案:c

说明:当原始数据类型传递给方法时,调用方法中变量的值保持不变。这种行为不依赖于原始值是否被重新分配其他值或通过加法、减法、乘法或其他操作进行修改。

Q3-7.

给定方法 eJava 的以下签名,选择正确重载此方法的选项:

public String eJava(int age, String name, double duration)
  1. private String eJava(int val, String firstName, double dur)
  2. public void eJava(int val1, String val2, double val3)
  3. String eJava(String name, int age, double duration)
  4. float eJava(double name, String age, byte duration)
  5. ArrayList<String> eJava()
  6. char[] eJava(double numbers)
  7. String eJava()

答案:c, d, e, f, g

说明:选项 (a) 是错误的。重载方法可以更改访问修饰符,但仅更改访问修饰符本身不会使其成为重载方法。此选项还更改了方法参数的名称,但这不会对方法签名造成任何影响。

选项 (b) 是错误的。重载方法可以更改方法的返回类型,但更改返回类型不会使其成为重载方法。

选项 (c) 是正确的。更改方法参数的类型位置会重载它。

选项 (d) 是正确的。更改方法的返回类型和更改方法参数的类型位置会重载它。

选项 (e) 是正确的。更改方法的返回类型并在参数列表中进行更改会重载它。

选项 (f) 是正确的。更改方法的返回类型并在参数列表中进行更改会重载它。

选项 (g) 是正确的。更改参数列表也会重载方法。

Q3-8.

给定以下代码,

class Course {
    void enroll(long duration) {
        System.out.println("long");
    }
    void enroll(int duration) {
        System.out.println("int");
    }
    void enroll(String s) {
        System.out.println("String");
    }
    void enroll(Object o) {
        System.out.println("Object");
    }
}

以下代码的输出是什么?

class EJavaGuru {
    public static void main(String args[]) {
            Course course = new Course();
            char c = 10;
            course.enroll(c);
            course.enroll("Object");
    }
}
  1. 编译错误
  2. 运行时异常
  3. int
    String
    
  4. long
    Object
    

答案:c

说明:代码中没有编译问题。您可以通过更改方法参数列表中的方法参数类型来重载方法。使用具有基类-派生类关系的数据类型的方法参数(ObjectString 类)是可以接受的。使用可以自动转换为对方的数据类型的方法参数(intlong)也是可以接受的。

当代码执行 course.enroll(c) 时,char 可以传递给接受 intlong 参数的两个重载 enroll 方法。char 被扩展为其最接近的类型——int——因此 course.enroll(c) 调用接受 int 的重载方法,打印 int。代码 course.enroll("Object") 传递了一个 String 值。尽管 String 也是 Object,但此方法调用传递给它的特定(非通用)类型。因此 course.enroll("Object") 调用接受 String 的重载方法,打印 String

Q3-9.

检查以下代码并选择正确的选项:

class EJava {
    public EJava() {
        this(7);
        System.out.println("public");
    }
    private EJava(int val) {
        this("Sunday");
        System.out.println("private");
    }
    protected EJava(String val) {
        System.out.println("protected");
    }
}
class TestEJava {
    public static void main(String[] args) {
        EJava eJava = new EJava();
    }
}
  1. EJava 定义了三个重载构造函数。

  2. EJava 定义了两个重载构造函数。私有构造函数不计入重载构造函数。

  3. 不同访问修饰符的构造函数不能相互调用。

  4. 代码打印以下内容:

  5. protected
    private
    public
    
  6. 代码打印以下内容:

  7. public
    private
    protected
    

    答案:a, d 说明:你可以像定义具有不同访问修饰符的重载方法一样定义具有不同访问修饰符的重载构造函数。但仅访问修饰符的变化不能用来定义重载方法或构造函数。private 方法或构造函数也被视为重载方法。以下代码行调用 EJava 的构造函数,该构造函数不接受任何方法参数:

    EJava eJava = new EJava();
    

    此类的无参数构造函数调用接受 int 参数的构造函数,后者又调用接受 String 参数的构造函数。因为具有 String 构造函数的构造函数没有调用任何其他方法,它打印 protected 并将控制权返回给接受 int 参数的构造函数。此构造函数打印 private 并将控制权返回给不接受任何方法参数的构造函数。此构造函数打印 public 并将控制权返回给 main 方法。

Q3-10.

选择错误的选项:

  1. 如果用户为公共类定义了私有构造函数,Java 会为该类创建一个公共默认构造函数。

  2. 获得默认构造函数的类没有重载构造函数。

  3. 用户可以重载类的默认构造函数。

  4. 以下类符合默认构造函数的条件:

  5. class EJava {}
    
  6. 以下类也符合默认构造函数的条件:

  7. class EJava {
            void EJava() {}
    }
    

答案:a, c

说明:选项 (a) 是错误的。如果用户为具有任何访问修饰符的类定义了构造函数,它就不再是有资格获得默认构造函数的候选者。

选项 (b) 是正确的。一个类只有在没有其他构造函数时才会获得默认构造函数。默认或自动构造函数不能与其他构造函数共存。

选项 (c) 是错误的。默认构造函数不能与其他构造函数共存。如果用户在类中没有定义任何构造函数,Java 编译器会自动创建一个默认构造函数。如果用户重新打开源代码文件并向类中添加一个构造函数,在重新编译后,将不会为该类创建默认构造函数。

选项 (d) 是正确的。因为这个类没有构造函数,Java 会为它创建一个默认构造函数。

选项 (e) 同样正确。这个类也没有构造函数,因此它有资格创建一个默认构造函数。以下不是构造函数,因为构造函数的返回类型不是 void:

void EJava() {}

它是一个常规且有效的函数,其名称与类名相同。

第四章。Java API 中的选定类和数组

本章涵盖的考试目标 你需要了解的内容
[9.2] 创建和操作字符串 如何使用=(赋值)和new运算符初始化 String 变量。使用运算符=, +=, !=, 和==与 String 对象。字符串字面量的池化。String 类的字面量值。使用 String 类的方法。不可变的 String 值。所有 String 方法都操作并返回一个新的 String 对象。
[3.2] 使用==equals()测试字符串与其他对象之间的相等性。 如何确定两个 String 对象之间的相等性。使用操作符==和方法equals()确定 String 对象相等性的区别。
[9.1] 使用 StringBuilder 类及其方法操作数据。 如何创建 StringBuilder 类以及如何使用它们常用的方法。StringBuilder 类和 String 类的区别。这两个类中定义的具有相似名称的方法的区别。
[4.1] 声明、实例化、初始化和使用一维数组。 如何使用单步和多步声明、实例化和初始化一维数组,以及每个步骤的注意事项。
[4.2] 声明、实例化、初始化和使用多维数组。 如何使用单步和多步声明、实例化和初始化多维数组,以及每个步骤的注意事项。访问非对称多维数组中的元素。
[9.4] 声明并使用给定类型的 ArrayList。 如何声明、创建和使用 ArrayList。使用 ArrayList 而不是数组的优势。使用添加、修改和删除 ArrayList 元素的方法。
[9.3] 使用java.time.LocalDateTimejava.time.LocalDatejava.time.LocalTimejava.time.format.DateTimeFormatterjava.time.Period类创建和操作日历数据。 如何使用 LocalDate、LocalTime 和 LocalDateTime 类存储日期和时间。识别实例化日期和时间对象的工厂方法。使用日期和时间类的实例方法。使用 Period 向日期对象添加或减去持续时间(天数、月份或年)。使用 DateTimeFormatter 格式化或解析日期和时间对象。

在 OCA Java SE 8 程序员 I 考试中,你将需要回答许多关于如何创建、修改和删除String对象、StringBuilder对象、数组、ArrayList对象和日期/时间对象的问题。为了准备这些问题,在本章中,我将提供有关您将使用的变量以存储这些对象值的见解,以及一些方法定义。这些信息应该有助于您正确应用所有方法。

在本章中,我们将涵盖以下内容:

  • 创建和操作StringStringBuilder对象

  • 使用StringStringBuilder类中的常用方法

  • 在单步和多个步骤中创建和使用一维和二维数组

  • 访问非对称多维数组中的元素

  • 声明、创建和使用 ArrayList 以及理解 ArrayList 相对于数组的优势

  • 使用添加、修改和删除 ArrayList 元素的方法

  • 使用 LocalDateLocalTimeLocalDateTime 类创建日期和时间对象

  • 使用 Period 类操作日期对象

  • 使用 DateTimeFormatter 格式化和解析日期和时间对象

让我们从 String 类开始。

4.1. 欢迎来到字符串类世界

[9.2] 创建和操作字符串

[3.2] 使用 ==equals() 测试字符串与其他对象的相等性

在本节中,我们将介绍 java.lang 包中定义的 String 类。String 类表示字符字符串。我们将创建 String 类型的对象,并使用其常用方法,包括 indexOf()substring()replace()charAt() 等。您还将学习如何确定两个 String 对象的相等性。

String 类可能是 Java API 中使用最频繁的类。您会发现 Java API 中的每个其他类都使用了这个类的实例。您认为您使用了 String 类多少次?不要回答这个问题——这就像试图数你的头发一样。

尽管许多开发者认为 String 类是其中最容易处理的之一,但这种看法可能会误导。例如,在 String"Shreya" 中,您认为 r 存储在哪个索引——第二个还是第三个?正确答案是第二个,因为字符串的第一个字母存储在索引 0 而不是索引 1。在本节中,您还将了解关于 String 类的许多其他事实。

让我们先创建这个类的新对象。

4.1.1. 创建字符串对象

您可以通过使用 new 操作符或使用 String 文字值(双引号内的值)来创建 String 类型的对象。您可以使用赋值操作符(=)将 String 文字值赋给 String 引用变量。但您可能已经注意到,Java 在存储和引用这些对象方面存在很大的差异。

让我们使用操作符 new 创建两个值为 "Paul"String 对象:

图 4.1 展示了之前的代码。

图 4.1. 使用操作符 new 创建的 String 对象始终引用单独的对象,即使它们存储相同的字符序列。

在之前的代码中,比较 String 引用变量 str1str2 输出 false。操作符 == 比较由变量 str1str2 指向的对象的地址。尽管这些 String 对象存储相同的字符序列,但它们引用的是存储在不同位置上的单独对象。

让我们使用赋值运算符(=)初始化两个 String 变量,值为 "Harry"。图 4.2 展示了变量 str3str4 以及这些变量所引用的对象。

图 4.2. 使用赋值运算符(=)创建的 String 对象可能指向同一个对象,如果它们存储相同的字符序列。

在前面的例子中,即使使用相同的字符序列创建,变量 str1str2 也指向不同的 String 对象。在变量 str3str4 的情况下,对象是在 String 对象池中创建和存储的。在池中创建新对象之前,Java 会搜索具有相似内容的对象。当执行以下代码行时,在 String 对象池中没有找到具有值 "Harry"String 对象:

String str3 = "Harry";

因此,Java 在变量 str3 指向的 String 对象池中创建了一个具有值 "Harry"String 对象。这一动作如图 4.3 所示。

图 4.3. 当 Java 无法在 String 对象池中找到 String 时执行的步骤序列

当执行以下代码行时,Java 能够在 String 对象池中找到具有值 "Harry"String 对象:

String str4 = "Harry";

在这种情况下,Java 不会创建一个新的 String 对象,变量 str4 指向现有的 String 对象 "Harry"。如图 4.4 所示,变量 str3str4 都指向对象池中的同一个 String 对象。

图 4.4. 当 Java 在 String 对象池中找到 String 时执行的动作序列

您也可以通过在双引号(")内包围一个值来创建 String 对象:

如果找到匹配的值,这些值将从 String 常量池中重用。如果没有找到匹配的值,JVM 将创建一个具有指定值的 String 对象并将其放置在 String 常量池中:

String morning1 = "Morning";
System.out.println("Morning" == morning1);

将前面的例子与以下例子进行比较,该例子使用 new 运算符和(仅)双引号创建 String 对象,然后比较它们的引用:

上述代码表明,存在于 String 常量池中的 String 对象的引用和不存在于 String 常量池中的 String 对象的引用不指向同一个 String 对象,即使它们定义了相同的 String 值。

注意

术语 String 常量池String 可以互换使用,并指代同一个 String 对象池。因为 String 对象是不可变的,所以 String 对象池也被称为 String 常量池。您可能在考试中看到这两个术语中的任何一个。

你还可以调用 String 类的其他重载构造函数,通过使用 new 运算符来创建其对象:

你也可以使用 StringBuilderStringBuffer 类来创建 String 对象:

因为 String 是一个类,你可以将它赋值为 null,如下例所示:

考试技巧

String 的默认值是 null

计算 String 对象

为了测试你对创建 String 对象的各种方式的了解,考试可能会询问你在一块给定的代码中创建了多少个 String 对象。计算以下代码中创建的 String 对象的总数,假设 String 常量池没有定义任何匹配的 String 值:

我会一步一步地带你分析代码,计算创建的 String 对象的总数:

  • 的代码中创建了一个值为 "Summer" 的新 String 对象。此对象没有被放入 String 常量池中。

  • 的代码中创建了一个值为 "Summer" 的新 String 对象并将其放入 String 常量池中。

  • 的代码中不需要创建任何新的 String 对象。它重用了 String 常量池中已经存在的值为 "Summer"String 对象。

  • 的代码中创建了一个值为 "autumn" 的新 String 对象并将其放入 String 常量池中。

  • 的代码中,重用了 String 常量池中的 "autumn" 值。它在 String 常量池中创建了一个值为 "summer"String 对象(注意字母的大小写差异——Java 是大小写敏感的,"Summer""summer" 不相同)。

  • 的代码中创建了一个值为 "Summer" 的新 String 对象。

之前的代码总共创建了五个 String 对象。

考试技巧

如果使用关键字 new 创建 String 对象,它总是导致创建一个新的 String 对象。以这种方式创建的 String 对象永远不会被池化。当使用赋值运算符将 String 文字赋给变量时,只有当 String 常量池中没有找到具有相同值的 String 对象时,才会创建一个新的 String 对象。

4.1.2. String 类是不可变的

记住 String 类不可变的概念是一个重要的要点。一旦创建,String 类对象的内部内容就永远不能被修改。String 对象的不可变性有助于 JVM 重复使用 String 对象,减少内存开销并提高性能。

如前所述,图 4.4 所示,JVM 创建了一个 String 对象池,这些对象可以在 JVM 中被多个变量引用。JVM 只能进行这种优化,因为 String 是不可变的。String 对象可以在多个引用变量之间共享,而不用担心它们值的变化。如果引用变量 str1str2 指向同一个 String 对象值 "Java",则 str1 不必担心在其生命周期内,值 "Java" 可能会通过变量 str2 被更改。

让我们快速看一下这个类的作者是如何实现 String 类不可变性的:

  • String 将其值存储在一个 private 类型的字符数组变量中(char value[])。数组的大小固定,一旦初始化就不会增长。

  • 这个 value 变量在 String 类中被标记为 final。请注意,final 是一个非访问修饰符,final 变量只能初始化一次。

  • String 中定义的任何方法都不会操作数组 value 的单个元素。

我将在接下来的章节中详细讨论这些点。

Java API 类的代码

为了让您更好地理解 StringStringBuilderArrayList 类的工作方式,我将解释用于存储这些对象值的变量,以及它们的一些方法定义。我的目的不是让您感到不知所措,而是为了做好准备。考试不会就这个主题提问您,但这些细节将帮助您保留与考试相关的信息,并在实际项目中实现类似的要求。

Java API 中定义的类的源代码包含在 Java 开发工具包(JDK)中。您可以通过解压缩 JDK 安装文件夹中的 src.zip 存档来访问它。

本节剩余部分将讨论 Java API 的作者如何在 String 类中实现不可变性。

String 使用字符数组来存储其值

下面是 Java 源代码文件(String.java)中 String 类的部分定义,包括用于存储 String 值字符的数组(相关代码用粗体表示):

数组的大小是固定的——一旦初始化就不能增长。

让我们创建一个类型为 String 的变量 name 并看看它是如何内部存储的:

String name = "Selvan";

图 4.5 展示了类 String 及其对象 name 的 UML 表示(左边的类图和右边的对象图),其中只有一个相关变量,value,它是一个 char 类型的数组,用于存储分配给 String 的字符序列。

图 4.5. String 类和具有 String 实例属性 valueString 对象的 UML 表示

如图 4.5 所示,StringSelvan存储在一个类型为char的数组中。在本章中,我将详细介绍数组以及数组如何将其第一个值存储在位置 0。

图 4.6 展示了Selvan如何存储为一个char数组。

图 4.6. String存储的字符与它们存储的位置的映射

图片

当您请求这个String返回位置 4 的字符时,你认为你会得到什么?如果你说是a而不是v,你就得到了正确答案(如图 4.6 所示)。

String 使用 final 变量来存储其值

用于存储String对象值的变量value被标记为final。请回顾一下来自String.java类的以下代码片段:

图片

final变量的基本特征是它只能初始化一次值。通过将变量value标记为finalString类确保它不能被重新赋值。

String类的方法不会修改字符数组

尽管我们无法像前一个章节中提到的将值重新赋给final char数组,但我们可以重新赋值其单个字符。哇——这难道意味着“字符串是不可变的”这个说法并不完全正确吗?

不,这个说法仍然是正确的。String类使用的char数组被标记为private,这意味着它不能被类外修改。String类本身也不会修改这个变量的值。

String类中定义的所有方法,如substringconcattoLower-CasetoUpperCasetrim等,看似会修改它们被调用的String对象的内容,实际上创建并返回一个新的String对象,而不是修改现有值。图 4.7 说明了Stringreplace方法的局部定义。

图 4.7. String类中replace方法的局部定义显示了该方法创建并返回一个新的String对象,而不是修改它所调用的String对象的值。

图片

我再次强调,来自String类的先前代码将帮助您将理论与代码联系起来,并理解特定概念是如何以及为什么工作的。如果您对某个概念在如何以及为什么工作方面理解得很好,您将能够更长时间地保留这些信息。

考试技巧

字符串是不可变的。一旦初始化,String值就不能修改。所有返回修改后的String值的String方法都返回一个新的带有修改值的String对象。原始的String值始终保持不变。

4.1.3. String类的方法

图 4.8 将考试中的方法分为几类:查询字符位置的方法、似乎修改String的方法以及其他方法。

图 4.8. String方法的分类

以这种方式对方法进行分类将有助于您更好地理解这些方法。例如,charAt()indexOf()substring() 方法查询 String 中单个字符的位置。substring()trim()replace() 方法似乎在修改 String 的值。

charAt()

您可以使用 charAt(int index) 方法来检索 String 中指定索引处的字符:

图 4.9 展示了前面的字符串,Paul

图 4.9。String 中存储的 "Paul" 字符序列及其对应的数组索引位置

由于最后一个字符放置在索引 3 处,以下代码将在运行时抛出异常:

System.out.println(name.charAt(4));
注意

作为快速介绍,运行时异常 是在代码执行过程中由 Java 运行时环境 (JRE) 确定的编程错误。这些错误是由于不恰当地使用其他代码片段(异常将在第七章中详细讨论)而发生的。前面的代码尝试访问一个不存在的索引位置,因此引发了异常。

indexOf()

您可以在 String 中搜索 charString 的出现。如果指定的 charString 在目标 String 中找到,则此方法返回第一个匹配的位置;否则,返回 -1

图 4.10 展示了前面的字符串 ABCAB

图 4.10。存储在 String 中的 "ABCAB" 字符

默认情况下,indexOf() 方法从目标 String 的第一个 char 开始搜索。如果您愿意,也可以设置起始位置,如下例所示:

substring()

substring() 方法有两种形式。第一种返回从您指定的位置到 String 结尾的子字符串,如下例所示:

图 4.11 展示了前面的示例。

图 4.11。String "Oracle"

您也可以使用此方法指定结束位置:

图 4.12 展示了 String"Oracle",包括 substring 方法的起始点和结束点。

图 4.12。substring 方法如何从起始位置查找直到结束位置的指定字符

一个有趣的观点是,substring 方法不包含结束位置处的字符。在上面的示例中,result 被分配了 ac 的值(位置 2 和 3 的字符),而不是 acl(位置 2、3 和 4 的字符)。这里有一个简单的方法来记住这个规则:

substring() 方法返回的 String 长度 = 结束索引 - 开始索引

考试提示

substring 方法不包含结束位置处的字符在其返回值中。

trim()

trim()方法通过移除String中所有前导和尾随的空白来返回一个新的String。空白是空白字符(换行符、空格或制表符)。

让我们定义并打印一个带有前导和尾随空白的String。 (在String前后打印的冒号确定了String的开始和结束。)

这里还有一个示例,展示了如何去除前导和尾随空白:

注意,此方法不会移除String内部的空格。

replace()

此方法将通过用另一个char替换所有出现的char来返回一个新的String。除了指定要替换的char外,你还可以指定一个字符序列——一个要替换的String

注意在此方法中传递的方法参数的类型:要么是char,要么是String。你不能混合这些参数类型,如下面的代码所示:

再次注意,此方法不会——或者不能——更改变量letters的值。检查以下代码行及其输出:

length()

你可以使用length()方法来检索String的长度。以下是一个展示其使用方法的示例:

考试技巧

String的长度比存储其最后一个字符的位置多一个数字。String "Shreya"的长度是 6,但它的最后一个字符a存储在位置 5,因为位置从 0 开始,而不是 1。

startsWith() 和 endsWith()

方法startsWith()确定一个String是否以指定的前缀开始,该前缀指定为String。你也可以指定是否要从String的开始位置或从特定位置开始搜索。如果找到匹配项,则此方法返回true,否则返回false

方法endsWith()测试一个String是否以特定的后缀结束。如果匹配则返回true,否则返回false

方法链式调用

在一行代码中使用多个String方法是一种常见的做法,如下所示:

方法是从左到右评估的。在这个例子中,首先执行的方法是replace,而不是concat

方法链式调用是考试作者最喜欢的主题之一。你肯定会在 OCA Java SE 8 程序员 I 考试中遇到关于方法链式调用的一个问题。

考试技巧

当链式调用时,方法是从左到右评估的。

注意,在String对象上调用方法链与将相同的操作应用于然后重新分配返回值到同一变量之间有一个区别:

由于String对象是不可变的,因此如果你在它们上执行方法,它们的值不会改变。当然,你可以将值重新分配给类型为String的引用变量。注意考试中的相关问题。

虽然下一个故事转折练习可能看起来很简单,只有两行代码,但外表可能具有欺骗性(答案见附录)。

故事转折 4.1

让我们修改上一节中使用的一些代码。在您的系统上执行此代码。哪个答案正确显示了其输出?

String letters = "ABCAB";
System.out.println(letters.substring(0, 2).startsWith('A'));
  1. true

  2. false

  3. AB

  4. ABC

  5. 编译错误

4.1.4. 字符串对象和运算符

在这个考试的所有运算符中,您只能使用少数几个与String对象一起使用:

  • 连接:++=

  • 等于:==!=

在本节中,我们将介绍连接运算符。我们将在下一节(4.1.5)介绍相等运算符。

连接运算符(++=)对String有特殊含义。Java 语言为这些运算符定义了针对String的附加功能。您可以使用运算符++=来连接两个String值。在幕后,字符串连接是通过使用StringBuilder(下一节介绍)或StringBuffer(类似于StringBuilder)类来实现的。

但请记住,String是不可变的。您不能修改任何现有String对象的价值。+运算符使您能够创建一个新的String类对象,其值等于多个String连接的值。检查以下代码:

图片

这里还有一个例子:

图片

你认为变量anotherStr的值为什么是22OCJA而不是1012OCJA+运算符可以与原始值一起使用,并且表达式num + val + aStr是从左到右评估的。以下是 Java 评估该表达式的步骤序列:

  • 将操作数numval相加得到22

  • 22OCJA连接以得到22OCJA

如果您希望将存储在变量numval中的数字视为String值,请按以下方式修改表达式:

图片

字符串连接的实用技巧

在我为 Java 程序员认证做准备的过程中,我学习了在字符串连接中,当连接的值的顺序改变时,输出如何变化。在工作中,这帮助我快速调试了一个将错误值记录到日志文件中的 Java 应用程序。我没有花很长时间就发现,有问题的代码行是logToFile("Shipped:" + numReceived() + inTransit());。这些方法单独返回正确的值,但这些方法的返回值没有被相加。它们被作为String连接,导致出现意外的输出。

一种解决方案是将int加法放在括号内,例如logToFile("Shipped:"+ (numReceived() + inTransit()));。此代码将记录文本"Shipped"num-Received()inTransit()方法返回的数值总和。

当您使用 += 连接 String 值时,请确保您使用的变量已经初始化(并且不包含 null)。看看以下代码:

236fig01_alt.jpg

4.1.5. 确定 String 的相等性

比较两个 String 值的正确方法是使用 String 类中定义的 equals 方法。如果与之比较的对象不是 null,是一个 String 对象,并且表示与它比较的对象相同的字符序列,则此方法返回 true 值。

equals 方法

以下列表展示了 Java API 中类 String 中定义的 equals 方法的定义方法。

列表 4.1. 类 Stringequals 方法的定义

236fig02_alt.jpg

在 列表 4.1 中,equals 方法接受一个类型为 Object 的方法参数,并返回一个 boolean 值。让我们回顾一下由类 String 定义的 equals 方法:

  • num-1.jpg 比较对象引用变量。如果引用变量相同,它们引用同一个对象。

  • num-2.jpg 比较方法参数的类型与该对象。如果传递给此方法的方法参数不是 String 类型,num-6.jpg 返回 false

  • num-3.jpg 检查正在比较的 String 值的长度是否相等。

  • num-4.jpg 比较了 String 值的各个字符。如果在任何位置找到不匹配,则返回 false。如果没有找到不匹配,num-5.jpg 返回 true

比较引用变量与实例值

检查以下代码:

237fig01_alt.jpg

运算符 == 比较引用变量,即变量是否引用同一个对象。因此,在之前的代码中 var1 == var2 打印 false。现在检查以下代码:

237fig02_alt.jpg

即使使用运算符 == 比较 var3var4 打印 true,您也 永远不要 使用此运算符来比较 String 值。变量 var3var4 引用同一个在 String 对象池中创建和共享的 String 对象。(我们已经在本章的 4.1.1 节 中讨论了 String 对象池。)即使两个对象存储相同的 String 值,运算符 == 也不总是会返回 true

考试技巧

运算符 == 比较引用变量是否引用同一个对象,而方法 equals 比较字符串值是否相等。始终使用 equals 方法来比较两个 String 的相等性。永远不要使用运算符 == 来完成此目的。

您可以使用运算符 != 来比较由 String 变量引用的对象的不等性。它是运算符 == 的逆运算。让我们比较运算符 != 与运算符 == 和方法 equals() 的用法:

238fig01_alt.jpg

以下示例使用 !=== 操作符以及 equals 方法来比较指向 String 常量池中相同对象的 String 变量:

238fig02_alt.jpg

如您所见,在先前的两个示例中,!= 操作符返回的是 == 操作符返回值的相反。

String 方法返回值的相等性

您认为方法返回的 String 值是否存储在 String 池中?当使用 == 操作符比较它们的变量引用时,它们会返回 true 吗?让我们来看看:

238fig03_alt.jpg

在前面的代码中,对 lang1.substring()lang2.subtring() 的调用将返回 "Ja"。但这些字符串值并没有存储在 String 池中。这是因为这些子字符串是使用 String 类的 substring 方法(以及其他 String 方法)中的 new 操作符创建的。这可以通过使用 == 操作符比较它们的引用变量来确认,它返回 false

考试技巧

注意考试中测试您使用 == 操作符与 String 类方法返回的 String 值的问题。因为这些值是使用 new 操作符创建的,所以它们不会被放置在 String 池中。

因为 String 是不可变的,所以我们还需要一个可变的字符序列,可以对其进行操作。让我们来看看 OCA Java SE 8 程序员 I 考试中的另一种字符串类型:StringBuilder

4.2. 可变字符串:StringBuilder

[9.1] 使用 StringBuilder 类及其方法操作数据

StringBuilder 类定义在 java.lang 包中,它有一个可变的字符序列。当您处理较大的字符串或经常修改字符串内容时,应使用 StringBuilder 类。这样做将提高您代码的性能。与 StringBuilder 不同,String 类有一个不可变的字符序列。每次您修改由 String 类表示的字符串时,您的代码都会创建新的 String 对象,而不是修改现有的对象。

考试技巧

你可以预期会有关于 StringBuilder 类的需求及其与 String 类的比较的问题。

让我们一起学习 StringBuilder 类的方法。因为 StringBuilder 代表一个可变的字符序列,所以对 StringBuilder 的主要操作与通过在末尾或特定位置添加另一个值、删除字符或更改特定位置的字符来修改其值有关。

4.2.1. StringBuilder 类是可变的

String 类相比,StringBuilder 类使用非 final char 数组来存储其值。以下是对类 AbstractStringBuilderStringBuilder 类的超类)的部分定义。它包括变量 valuecount 的声明,分别用于存储 StringBuilder 的值及其长度(相关代码以粗体显示):

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char value[];
    /**
     * The count is the number of characters used.
     */
    int count;
//.. rest of the code
}

在接下来的几节中讨论 StringBuilder 类的方法时,这些信息将很有用。

4.2.2. 创建 StringBuilder 对象

您可以使用多个重载构造函数创建 StringBuilder 类的对象,如下所示:

构造了一个没有任何字符且初始容量为 16 个字符的 StringBuilder 对象。 构造了一个包含与传递给它的 StringBuilder 对象相同字符集的 StringBuilder 对象。 构造了一个没有任何字符且初始容量为 50 个字符的 StringBuilder 对象。 构造了一个初始值与 String 对象相同的 StringBuilder 对象。图 4.13 展示了具有值 Shreya GuptaStringBuilder 对象 sb4

图 4.13. StringBuilder 对象及其对应的存储位置字符值

当您使用其默认构造函数创建 StringBuilder 对象时,以下代码在幕后执行以初始化在 StringBuilder 类本身中定义的数组 value

当您通过传递一个 String 来创建 StringBuilder 对象时,以下代码在幕后执行以初始化数组值:

StringBuilder 类对象的创建是下一个 Twist in the Tale 练习的基础。在这个练习中,您的任务是查找 Java API 文档或 Java 源代码来回答问题。您可以通过以下几种方式访问 Java API 文档:

以下 Twist in the Tale 练习的答案是附录中给出的。

Twist in the Tale 4.2

查看 Java API 文档或 Java 源代码文件,并回答以下问题:

以下哪个选项(只有一个正确答案)正确地创建了一个具有默认容量为 16 个字符的 StringBuilder 类对象?

  1. StringBuilder name = StringBuilder.getInstance();

  2. StringBuilder name = StringBuilder.createInstance();

  3. StringBuilder name = StringBuilder.buildInstance();

  4. 以上均不正确

4.2.3. StringBuilder 类的方法

你会很高兴地了解到,在 StringBuilder 类中定义的许多方法与 String 类中的版本工作方式完全相同——例如,charAtindexOfsubstringlength 等方法。我们不会再次讨论 StringBuilder 类中的这些方法。在本节中,我们将讨论 StringBuilder 类的其他主要方法:appendinsertdelete

图 4.14 展示了此类方法的分类。

图 4.14. StringBuilder 方法的分类

append()

append 方法将指定的值添加到 StringBuilder 对象的现有值末尾。因为你可能想要将来自多个数据类型的数据添加到 StringBuilder 对象中,所以该方法被重载,以便它可以接受任何类型的数据。

此方法接受所有原始类型、Stringchar 数组以及 Object 作为方法参数,如下例所示:

你可以按以下方式附加完整的 char 数组、StringBuilderString 或其子集:

因为方法 append 也接受类型为 Object 的方法参数,所以你可以传递来自 Java API 或你自定义的任何对象:

上一段代码的输出是

JavaPerson@126b249

在这个输出中,跟随 @ 符号的十六进制值(126b249)可能因系统而异。

当你将对象的值附加到 StringBuilder 时,append 方法会调用静态的 String.valueOf() 方法。接受 Object 参数的版本,如果参数为 null,则返回四字母字符串null;否则,它调用其 toString 方法。如果 toString 方法已被类覆盖,则 append 方法会将它返回的 String 值添加到目标 StringBuilder 对象中。如果没有覆盖 toString 方法,则执行类 Object 中定义的 toString 方法。为了你的信息,类 ObjecttoString 方法的默认实现返回类的名称,后跟 @ 字符和对象的哈希码的无符号十六进制表示(由对象的 hashCode 方法返回的值)。

考试提示

对于没有覆盖 toString 方法的类,append 方法会导致将类 Object 中定义的方法 toString 的默认实现输出附加到(如果参数不是 null)。

快速查看 StringBuilder 类的 append 方法的工作方式很有趣。以下是对接受 boolean 参数的 append 方法的部分代码列表(如注释中所述):

num-1.jpgnum-2.jpg 确定数组 value 是否可以容纳与布尔字面值 true 对应的四个额外字符。在 num-2.jpg 中,对 expand-Capacity() 的调用会增加数组 value(用于存储 StringBuilder 对象的字符)的容量(如果它不够大)。num-3.jpg 将布尔值 true 的单个字符添加到数组 value 中。

insert()

insert 方法与 append 方法一样强大。它也存在多种变体(即:重载方法),可以接受任何数据类型。appendinsert 方法之间的主要区别在于,insert 方法允许您在特定位置插入所需数据,而 append 方法只允许您在 StringBuilder 对象的末尾添加所需数据:

244fig01_alt.jpg

图 4.15 展示了前面的代码。

图 4.15. 在 StringBuilder 中使用 insert 方法插入 char

04fig15.jpg

String 对象一样,StringBuilder 的第一个字符存储在位置 0。因此,前面的代码在位置 2 插入了字母 r,该位置被字母 n 占据。您也可以插入完整的 char 数组、StringBufferString 或其子集,如下所示:

244fig02_alt.jpg

图 4.16 展示了前面的代码。

图 4.16. 在 StringBuilder 中插入 String 的子字符串

04fig16_alt.jpg

考试提示

StringBuilder 中插入值时,请注意起始和结束位置。StringBuilder 中定义的 insert 方法的多种变体可能会让您感到困惑,因为它们可以用来插入单个或多个字符。

delete()deleteCharAt()

delete 方法删除指定 StringBuilder 的子字符串中的字符。deleteCharAt 方法删除指定位置的 char。以下是一个显示 delete 方法的示例:

245fig01_alt.jpg

num-1.jpg 删除位置 2 和 3 的字符。delete 方法不会删除位置 4 的字母。图 4.17 展示了前面的代码。

图 4.17. 方法 delete(2,4) 不会删除位置 4 的字符。

04fig17_alt.jpg

方法 deleteCharAt 很简单。它删除单个字符,如下所示:

245fig02_alt.jpg

考试提示

deleteCharAtinsert 方法的组合可能会相当令人困惑。

trim()

String 类不同,StringBuilder 类没有定义 trim 方法。尝试使用此类使用它将阻止您的代码编译。我在这里描述一个不存在的方法的唯一原因是为了避免任何混淆。

reverse()

如其名所示,reverse 方法反转 StringBuilder 中字符的顺序:

246fig01_alt.jpg

考试提示

您不能使用 reverse 方法来反转 StringBuilder 的子字符串。

replace()

与在类 String 中定义的replace方法不同,类 StringBuilder 中的replace方法通过指定位置来替换一系列字符,如下例所示:

图片

图 4.18 显示了在类StringStringBuilder中定义的replace方法的比较。

图 4.18. 比较String(左)和StringBuilder(右)中的replace方法。String中的replace方法接受要替换的字符。StringBuilder中的replace方法接受要替换的位置。

图片

subSequence()

除了使用substring方法外,您还可以使用subSequence方法来检索StringBuilder对象的子序列。此方法返回CharSequence类型的对象:

图片

subsequence方法不会修改StringBuilder对象的现有值。

4.2.4. 关于类 StringBuffer 的简要说明

虽然 OCA Java SE 8 程序员 I 考试目标没有提到类StringBuffer,但你可能在 OCA 考试的错误答案列表中看到它。

StringBufferStringBuilder提供相同的功能,但有一个区别:StringBuffer类的方法在必要时是同步的,而StringBuilder类的方法不是。这意味着什么?当您使用StringBuffer类时,只有多个线程中的一个可以执行您的该方法。这种安排可以防止这些(同步)方法修改的实例变量值出现不一致。但这也引入了额外的开销,因此使用同步方法和StringBuffer类会影响您代码的性能。

StringBuilder提供与StringBuffer相同的功能,但没有额外的同步方法功能。通常您的代码不会被多个线程访问,因此不需要线程同步的开销。如果您需要从多个线程访问您的代码,请使用StringBuffer;否则请使用StringBuilder

4.3. 数组

[4.1] 声明、实例化、初始化和使用一维数组

[4.2] 声明、实例化、初始化和使用多维数组

在本节中,我将介绍一维数组和多维数组的声明、分配和初始化。您将了解原始数据类型数组和对象数组之间的区别。

4.3.1. 什么是数组?

数组是一个存储值集合的对象。数组本身是对象这一事实往往被忽视。我将重申:数组本身就是一个对象;它存储它所存储数据的引用。数组可以存储两种类型的数据:

  • 基本数据类型的集合

  • 一组对象集合

原始数据数组存储构成原始值的值集合。(对于原始数据,没有要引用的对象。)对象数组存储值集合,实际上是指向堆内存地址或指针。这些地址指向(引用)数组所存储的对象实例,这意味着对象数组存储引用(到对象),而原始数组存储原始值。

数组的成员定义在连续(连续)的内存位置中,因此提供了改进的访问速度。(如果您能快速访问所有相邻的学生,您应该能够快速访问一个班级的所有学生。)

以下代码创建了一个原始数据数组和对象数组:

图片

我将在稍后讨论创建数组的细节。上一个示例展示了创建数组的一种方法。图 4.19 展示了intArrayobjArray数组。与intArray不同,objArray存储了对String对象的引用。

图 4.19. int原始数据类型数组和String对象数组

图片

注意

数组是对象,指向原始数据类型或其他对象的集合。

在 Java 中,您可以定义一维和多维数组。一维数组是一个指向标量值集合的对象。二维(或更多)数组称为多维数组。二维数组是指一个集合中的对象,其中每个对象都是一维数组。同样,三维数组是指二维数组的集合,依此类推。图 4.20 描述了一维数组和多维数组(二维和三维)。

图 4.20. 一维和多维(二维和三维)数组

图片

注意,多维数组可能或可能不包含每行或每列相同数量的元素,如图 4.20 中的二维数组所示。

创建数组涉及三个步骤,如下所示:

  • 声明数组

  • 分配数组

  • 初始化数组元素

您可以通过使用单独的代码行执行前面的步骤或将这些步骤组合在同一行代码中创建数组。让我们从第一种方法开始:在单独的代码行上完成每个步骤。

4.3.2. 数组声明

数组声明包括数组类型和数组变量,如图 4.21 所示。数组可以存储的对象类型取决于其类型。数组类型后面跟着一个或多个空方括号对[]

图 4.21. 数组声明包括数组类型和数组变量

图片

要声明一个数组,指定其类型,然后是数组变量的名称。以下是一个声明intString值数组的示例:

方括号对的数量表示数组嵌套的深度。Java 不对数组嵌套的级别设置任何理论上的限制。方括号可以跟在数组类型或其名称之后,如图 4.22 所示。

图 4.22. 方括号可以跟在变量名称或其类型之后。在多维数组的情况下,它可以跟在两者之后。

注意

在数组声明中,将方括号放在类型旁边(如int[]int[][])是首选的,因为它通过显示正在使用的数组类型使代码更容易阅读。

数组声明仅创建一个引用null的变量,如图 4.23 所示。

图 4.23. 数组声明创建一个引用null的变量。

因为在声明数组时没有创建数组元素,所以使用声明定义数组的大小是不合法的。以下代码无法编译:

数组类型可以是以下任何一种:

  • 原始数据类型

  • 接口

  • 抽象类

  • 具体类

我们之前声明了一个int原始数据类型的数组和一个具体的类String。我将在 4.3.7 节中讨论一些使用抽象类和接口的复杂示例。

注意

数组可以是除了null之外任何数据类型。

4.3.3. 数组分配

如其名所示,数组分配将为数组的元素分配内存。当你为数组分配内存时,你应该指定其维度,例如数组应存储的元素数量。请注意,一旦分配,数组的尺寸就不能扩展或缩小。以下是一些示例:

因为数组是一个对象,所以使用关键字new进行分配,后面跟它存储的值的类型,然后是其尺寸。如果你没有指定数组的尺寸,或者将数组尺寸放在=符号的左侧,代码将无法编译,如下所示:

数组的尺寸必须评估为一个int类型的值。你不能创建一个其尺寸指定为浮点数的数组。以下代码行无法编译:

Java 接受一个表达式来指定数组的尺寸,只要它评估为一个int类型的值。以下都是有效的数组分配:

让我们按照以下方式分配多维数组multiArr

你也可以通过只定义第一个方括号中的尺寸来分配多维数组multiArr

有趣的是要注意,当通过定义单维或两个维度的尺寸来分配多维数组multiArr时,所发生的情况之间的差异。这种差异如图 4.24 所示。

图 4.24. 当使用仅为其一个维度提供值和为其两个维度都提供值来分配二维数组时,数组分配的差异

你不能如下分配一个多维数组:

无法编译,因为在赋值运算符(=)两侧的方括号数量不匹配。编译器要求赋值运算符右侧使用[][],但它只找到了[] 无法编译,因为你不能在不包括第一个方括号中的大小并在第二个方括号中定义大小的情况下分配多维数组。

一旦分配,数组元素将存储它们的默认值。对于存储对象的数组,所有分配的数组元素都存储 null。对于存储原始值的数组,默认值取决于它们存储的确切数据类型。

考试技巧

一旦分配,所有数组元素将存储它们的默认值。存储对象的数组元素默认为 null。存储原始数据类型的数组元素存储 0(对于整数类型 byteshortintlong);0.0(对于十进制类型 floatdouble);false(对于 boolean);或 \u0000(对于 char 数据类型)。

4.3.4. 数组初始化

你可以这样初始化一个数组:

在前面的代码中, 使用 for 循环初始化 intArray 数组,使其包含所需的值。 在不使用 for 循环的情况下初始化单个数组元素。请注意,所有数组对象都使用它们的公共不可变字段 length 来访问它们的数组大小。

类似地,可以如下声明、分配和初始化一个 String 数组:

当你初始化一个二维数组时,你可以使用嵌套的 for 循环来初始化其数组元素。同时请注意,要访问二维数组中的元素,你应该使用两个数组位置值,如下所示:

当你尝试访问一个不存在的数组索引位置时会发生什么?以下代码创建了一个大小为 2 的数组,但试图在索引 3 处访问其数组元素:

之前的代码将抛出运行时异常,ArrayIndexOutOfBounds-Exception。对于大小为 2 的数组,唯一有效的索引位置是 0 和 1。其余的所有数组索引位置将在运行时抛出 ArrayIndexOutOfBoundsException 异常。

注意

如果你不能立即吸收与异常相关的所有信息,请不要担心。异常将在第七章中详细讲解。章节 7。

Java 编译器不会检查你尝试访问数组元素时的索引位置范围。你可能惊讶地发现,以下代码行将成功编译,尽管它使用了负数组索引值:

虽然前面的代码可以成功编译,但在运行时将抛出Array-IndexOutOfBoundsException异常。如果你不传递charbyteshortint数据类型(包装类不在此考试范围内,我也没有包括在内),访问数组元素的代码将无法编译:

考试技巧

访问数组索引的代码如果传递了无效的数组索引值,将会抛出运行时异常。访问数组索引的代码如果没有使用charbyteshortint,将无法编译。

此外,你不能删除数组位置。对于对象数组,你可以将位置设置为null值,但这不会删除数组位置:

创建了一个String类型的数组,并使用四个String值进行初始化。 将数组索引 2 的值设置为null 遍历所有数组元素。如下面的输出所示,打印了四个(而不是三个)值:

Autumn
Summer
null
Winter

4.3.5. 结合数组声明、分配和初始化

你可以将之前提到的所有数组声明、分配和初始化步骤合并为一步,如下所示:

int intArray[] = {0, 1};
String[] strArray = {"Summer", "Winter"};
int multiArray[][] = { {0, 1}, {3, 4, 5} };

注意到前面的代码

  • 不使用关键字new来初始化数组

  • 不指定数组的大小

  • 使用一对花括号定义一维数组的值,并使用多对花括号定义多维数组的值

数组声明、分配和初始化的所有先前步骤也可以以下这种方式结合:

int intArray2[] = new int[]{0, 1};
String[] strArray2 = new String[]{"Summer", "Winter"};
int multiArray2[][] = new int[][]{ {0, 1}, {3, 4, 5}};

与第一种方法不同,前面的代码使用关键字new来初始化数组。

如果你尝试使用前面提到的方法指定数组的大小,代码将无法编译。以下是一些示例:

int intArray2[] = new int[2]{0, 1};
String[] strArray2 = new String[2]{"Summer", "Winter"};
int multiArray2[][] = new int[2][]{ {0, 1}, {3, 4, 5}};
考试技巧

当你在单一步骤中结合数组声明、分配和初始化时,你不能指定数组的大小。数组的大小通过分配给数组值的数量来计算。

另一个需要注意的重要点是,如果你使用两行代码分别声明和初始化数组,你将使用关键字new来初始化值。以下代码行是正确的:

int intArray[];
intArray = new int[]{0, 1};

但你不能错过关键字new,并按以下方式初始化你的数组:

int intArray[];
intArray = {0, 1};

4.3.6. 非对称多维数组

在本节的开始,我提到多维数组可以是非对称的。数组可以为每一行定义不同数量的列。

以下示例是一个非对称的二维数组:

String multiStrArr[][] = new String[][]{
                                        {"A", "B"},
                                        null,
                                        {"Jan", "Feb", "Mar"},
                                    };

图 4.25 显示了此非对称数组。

图 4.25. 非对称数组

如你可能已经注意到的,multiStrArr[1]引用了一个null值。尝试访问此数组的任何元素,例如multiStrArr[1][0],将抛出异常。这把我们带到了下一个“故事转折”练习(答案在附录中)。

故事转折 4.3

修改前一个例子中使用的部分代码如下:

Line1> String multiStrArr[][] = new String[][]{
Line2>                                        {"A", "B"},
Line3>                                        null,
Line4>                                        {"Jan", "Feb", null},
Line5>                                        };

以下哪个选项对于前面的代码是正确的?

  1. 第 4 行的代码与{"Jan", "Feb", null, null},相同。

  2. multiStrArr[2][2]处没有存储值。

  3. multiStrArr[1][1]处没有存储值。

  4. 数组multiStrArr是不对称的。

4.3.7. 接口、抽象类和Object类的数组

在数组声明部分,我提到数组的类型也可以是接口或抽象类。这些数组的元素存储什么值?让我们看看一些例子。

接口类型

如果数组类型是接口,其元素可以是null或者实现了相关接口类型的对象。例如,对于接口MyInterface,数组interfaceArray可以存储MyClass1MyClass2类的对象引用:

interface MyInterface {}
class MyClass1 implements MyInterface {}

class MyClass2 implements MyInterface {}
class Test {
MyInterface[] interfaceArray = new MyInterface[]
                                  {
                                      new MyClass1(),
                                      null,
                                      new MyClass2()
                                  };
}
抽象类类型

如果数组的类型是abstract类,其元素可以是null或者扩展相关abstract类的具体类的对象:

图片

接下来,我将讨论一个特殊情况,即数组的类型是Object

Object

因为所有类都扩展了java.lang.Object类,所以类型为java.lang.Object的数组的元素可以引用任何对象。以下是一个例子:

图片

图片是有效的代码。因为数组是一个对象,java.lang.Object数组中的元素可以引用另一个数组。图 4.26 说明了之前创建的数组objArray

图 4.26. Object类的数组

图片

4.3.8. 数组的成员

数组对象有以下public成员:

  • length—公共变量length存储数组的元素数量。

  • clone()—此方法覆盖了在Object类中定义的clone方法,但不抛出检查异常。此方法的返回类型与数组的类型相同。例如,对于类型为Type[]的数组,此方法返回Type[]

  • 继承的方法—Object类继承的方法,除了clone方法。

如前所述,String类使用length()方法来获取其长度。对于数组,你可以使用数组的变量length来确定数组元素的个数。在考试中,你可能会被试图使用变量length访问Stringlength的代码所迷惑。注意访问其长度时使用的正确类和成员组合:

  • String—使用length()方法获取长度

  • 数组— 使用length变量确定元素数量

我有一个记住这个规则很有趣的方法。与数组不同,你会在String对象上调用很多方法。所以你使用length()方法来获取String的长度,使用length变量来获取数组的长度。

4.4. ArrayList

[9.4] 声明和使用指定类型的 ArrayList

在本节中,我将介绍如何使用ArrayList,其常用方法和它相对于数组提供的优势。

OCA Java SE 8 程序员 I 考试仅涵盖 Java 集合 API 中的一个类:ArrayList。Java 集合 API 中的其余类在 OCP Java SE 8 程序员 II 考试(考试编号 1Z0-809)中涵盖。将此类包含在 Java Associate 考试中的一个原因可能是所有 Java 程序员使用此类的频率。

ArrayList是集合框架中最广泛使用的类之一。它提供了由数组列表数据结构提供的最佳功能组合。列表中最常用的操作是向列表添加项目修改列表中的项目从列表中删除项目遍历项目

Java 开发者经常问的一个问题是,“为什么我要费心使用ArrayList,当我已经在数组中存储了相同类型的对象时?”答案在于ArrayList的使用简便性。这是一个重要的问题,本考试包含关于使用ArrayList的实际原因的明确问题。

你可以将ArrayList与可变数组进行比较。正如你所知,一旦创建,你无法增加或减少数组的大小。另一方面,ArrayList在元素添加到或从其中删除时会自动增加或减少大小。此外,与数组不同,你不需要指定初始大小来创建ArrayList

让我们用现实世界中的对象比较ArrayList和数组。就像气球在充气或放气时可以增加或减少大小一样,ArrayList在添加或从其中删除值时也可以增加或减少大小。一个比较是板球,因为它有一个预定义的大小。一旦创建,就像数组一样,它的大小不能增加或减少。

这里是ArrayList的一些其他重要属性:

  • 它实现了List接口。

  • 它允许添加null值。

  • 它实现了所有列表操作(添加、修改和删除值)。

  • 它允许添加重复值。

  • 它维护其插入顺序。

  • 你可以使用IteratorListIterator遍历ArrayList中的项目。

  • 它支持泛型,使其类型安全。(你必须使用其声明来声明应添加到ArrayList中的元素类型。)

4.4.1. 创建一个 ArrayList

以下示例展示了如何创建ArrayList

java.util没有隐式导入到你的类中,这意味着num-1.jpg导入的是之前定义在CreateArrayList类中的ArrayList类。要创建一个ArrayList,你需要通知 Java 你想要存储在这个对象集合中的对象类型。num-2.jpg声明了一个名为myArrListArrayList,它可以存储由类名String指定的String对象。注意,在num-2.jpg的代码中,String出现了两次,一次在等号左边,一次在右边。你认为第二个看起来重复吗?恭喜你,Oracle 也同意你的看法。从 Java 版本 7 开始,你可以在等号右边省略对象类型,并按如下方式创建一个ArrayList

260fig01_alt.jpg

许多开发者仍在使用 Java SE 版本 7 之前的版本,所以你很可能会看到一些开发者仍在使用创建ArrayList的较老方式。

看看当你执行前面的语句来创建一个ArrayList时幕后发生了什么(在 Java 源代码中)。因为你没有向ArrayList类的构造函数传递任何参数,所以它的无参构造函数将被执行。检查以下在ArrayList.java类中定义的无参构造函数的定义:

/**
  * Constructs an empty list with an initial capacity of ten.
  */
public ArrayList() {
    this(10);
}

因为你可以使用ArrayList来存储任何类型的Object,所以ArrayList定义了一个类型为Object[]的实例变量elementData来存储所有单个元素。以下是ArrayList类的一个部分代码列表:

/**
  * The array buffer into which the elements of the ArrayList are stored.
  * The capacity of the ArrayList is the length of this array buffer.
  */
private transient Object[] elementData;

图 4.27说明了在ArrayList对象中显示的elementData变量。

图 4.27. ArrayList对象中显示的elementData变量

04fig27.jpg

这里是ArrayList类(ArrayList.java)中构造函数的定义,它初始化之前定义的实例变量elementData

/**
  * Constructs an empty list with the specified initial capacity.
  *
  * @param   initialCapacity   the initial capacity of the list
  * @exception IllegalArgumentException if the specified initial capacity
  *            is negative
  */
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
    this.elementData = new Object[initialCapacity];
}

等一下。你注意到ArrayList使用数组来存储其单个元素吗?这让你想知道为什么你需要另一个类,因为它使用了一个你已知如何操作的类型(确切地说,是数组)?简单的答案是,你不想重新发明轮子。

例如,回答以下问题:解码图像时,你更倾向于以下哪个选项?

  • 使用字符创建自己的类来解码图像

  • 使用提供相同功能的现有类

显然,选择第二个选项是有意义的。如果你使用一个提供相同功能的现有类,你将获得更多的好处,而工作量更少。同样的逻辑也适用于ArrayList。它提供了使用数组的所有好处,而没有使用数组的任何缺点。它看起来和表现就像一个可扩展的、可修改的数组。

注意

ArrayList使用数组来存储其元素。它为你提供了动态数组的函数。

在接下来的几节中,我将介绍如何添加、修改、删除和访问 ArrayList 的元素。让我们从向 ArrayList 添加元素开始。

4.4.2. 向 ArrayList 添加元素

让我们先通过以下方式向 ArrayList 添加 String 对象:

你可以向 ArrayList 的末尾或指定位置添加一个值。 将元素添加到 list 的末尾。 将一个元素添加到 list 的位置 2。请注意,ArrayList 的第一个元素存储在位置 0。因此,在 处,String 文字值 "three" 将被插入到位置 2,该位置原本由文字值 "four" 占据。当一个值被添加到已经被另一个元素占据的位置时,值会移动一个位置以容纳新添加的值。在这种情况下,文字值 "four" 移动到位置 3,为文字值 "three" 让出空间(参见 图 4.28)。

图 4.28. 向 ArrayList 添加元素到末尾和指定位置的代码

让我们看看当你向 ArrayList 添加一个元素时,幕后发生了什么。以下是 ArrayList 类中 add 方法的定义:

/**
  * Appends the specified element to the end of this list.
  *
  * @param e element to be appended to this list
  * @return <tt>true</tt> (as specified by {@link Collection#add})
  */
public boolean add(E e) {
    ensureCapacity(size + 1);    // Create another array with
                                 // the increased capacity
                                 // and copy existing elements to it.

    elementData[size++] = e;     // Store the newly added variable
                                 // reference at the
                                 // end of the list.
    return true;
}

当你将一个元素添加到列表的末尾时,ArrayList 首先检查其实例变量 elementData 是否在末尾有一个空槽。如果末尾有空槽,它将元素存储在第一个可用的空槽中。如果没有空槽,方法 ensureCapacity 会创建一个具有更高容量的新数组,并将现有值复制到这个新创建的数组中。然后,它将新添加的值复制到数组中第一个可用的空槽。

当你在特定位置添加一个元素时,ArrayList 会创建一个新的数组(只有当剩余空间不足时),并将所有其他元素插入到你指定的位置之外。如果你指定的位置右侧有任何后续元素,它们会向右移动一个位置。然后,它将新元素添加到请求的位置。

实用技巧

理解一个类以特定方式工作的原因和方法,在认证考试和你的职业生涯中都会大有裨益。这种理解应该有助于你更长时间地保留信息,并帮助你回答认证考试中旨在验证你使用此类实际知识的问题。最后,类的内部工作原理将使你能够在工作场所使用特定类时做出明智的决定,并编写高效的代码。

4.4.3. 访问 ArrayList 的元素

在我们修改或删除 ArrayList 的元素之前,让我们看看如何访问它们。要访问 ArrayList 的元素,你可以使用 get()、增强型 for 循环、IteratorListIterator

以下代码使用增强型for循环(访问元素的代码用粗体表示)访问并打印ArrayList中的所有元素:

之前代码的输出如下:

One
Two
Three
Four

定义了增强型for循环来访问myArrList中的所有元素。

让我们看看如何使用ListIterator遍历ArrayList中的所有值:

获取与ArrayList myArrList关联的迭代器。 在迭代器上调用hasNext方法来检查myArrList中是否存在更多元素。如果其元素存在更多,hasNext方法返回boolean true值,否则返回false 在迭代器上调用next方法从myArrList获取下一个项目。

之前的代码打印出与之前代码相同的结果,即使用增强型for循环访问ArrayList元素的代码。ListIterator不包含对ArrayList当前元素的引用。ListIterator为您提供了一个方法(hasNext)来检查是否还有更多元素存在于ArrayList中。如果为true,您可以使用next()方法提取其下一个元素。

注意,ArrayList保留了其元素的插入顺序。ListIterator和增强型for循环将按您添加它们的顺序返回元素。

考试提示

ArrayList保留了其元素的插入顺序。IteratorListIterator和增强型for循环将按它们被添加到ArrayList中的顺序返回元素。迭代器(IteratorListIterator)允许您在迭代ArrayList时删除元素。在迭代ArrayList时使用for循环无法删除元素。

4.4.4. 修改 ArrayList 的元素

您可以通过替换ArrayList中的现有元素或修改其所有现有值来修改ArrayList。以下代码使用set方法替换ArrayList中的元素:

之前代码的输出如下:

One
One and Half
Three

您也可以通过访问其单个元素来修改ArrayList中现有的值。由于String是不可变的,让我们用StringBuilder来尝试一下。以下是代码:

此代码的输出如下:

One3
Two3
Three5

访问myArrList中的所有元素,并通过将其长度追加到它上来修改元素值。通过再次访问myArrList元素来打印修改后的值。

4.4.5. 删除 ArrayList 的元素

ArrayList定义了两个方法来删除其元素,如下所示:

  • remove(int index)—此方法从列表中删除指定位置的元素。

  • remove(Object o)—此方法从列表中删除指定元素的第一种出现,如果存在的话。

让我们看看一些使用这些删除方法的代码示例:

上一段代码的输出如下:

One
Three
Four
One
Four

尝试从myArrList中移除具有值"Four"StringBuilder。由于对象引用比较的方式,指定元素的移除失败。对象是通过它们的equals()方法进行比较的,而StringBuilder类没有重写这个方法。因此,如果两个StringBuilder对象的引用(存储它们的变量)指向同一个对象,则这两个StringBuilder对象是相等的。您始终可以在自己的类中重写equals方法来改变这种默认行为。以下是一个使用MyPerson类的示例:

处,MyPerson类中的equals方法重写了Object类中的equals方法。如果传递给此方法的参数是null值,则返回false。如果传递给它的对象是具有匹配实例变量name值的MyPerson对象,则返回true

处,方法remove通过调用方法equals比较对象以确定相等性,从而从myArr-List中移除名为Paul的元素。如前所述,方法remove在从ArrayList中移除元素之前会调用equals方法进行比较。

当从ArrayList中移除元素时,剩余的元素会重新排列到它们正确的位置。这种变化是必要的,以便检索所有剩余元素在其正确的新位置。

4.4.6. ArrayList 的其他方法

让我们简要讨论一下在ArrayList中定义的其他重要方法。

向 ArrayList 添加多个元素

您可以使用以下重载的addAll方法,将多个元素从一个ArrayList或任何其他是Collection子类的类中添加到另一个ArrayList中:

  • addAll(Collection<? extends E> c)

  • addAll(int index, Collection<? extends E> c)

方法addAll(Collection<? extends E> c)将指定集合中的所有元素按指定集合的Iterator返回的顺序追加到该列表的末尾。如果您不熟悉泛型,并且这个方法的参数看起来令您感到害怕,请不要担心——集合 API 中的其他类不在这个考试范围内。

方法addAll(int index, Collection<? extends E> c)将指定集合中的所有元素插入到该列表的指定位置。

在以下代码示例中,ArrayList yourArrList的所有元素从位置 1 开始插入到ArrayList myArrList中:

上一段代码的输出如下:

One
Three
Four
Two

yourArrList中的元素不会被移除。现在可以通过myArrList引用存储在yourArrList中的对象。

如果你修改这些列表中公共的对象引用,myArrListyourArrList,会发生什么?这里有两种情况:在第一种情况下,你使用任一列表重新分配对象引用。在这种情况下,第二个列表中的值将保持不变。在第二种情况下,你修改任何公共列表元素的内部结构——在这种情况下,更改将在两个列表中反映出来。

考试技巧

这也是考试出题者最喜欢的主题之一。在考试中,你可能会遇到一个将相同对象引用添加到多个列表中,然后测试你对所有列表中相同对象和引用变量状态理解的问题。如果你对此问题有任何疑问,请参阅关于引用变量的部分(第 2.3 节)。

是时候进行我们的下一个故事转折练习了。让我们修改一下我们在之前的例子中使用的一些代码,看看它如何影响输出(答案见附录)。

故事转折 4.4

以下代码的输出是什么?

ArrayList<String> myArrList = new ArrayList<String>();
String one = "One";
String two = new String("Two");
myArrList.add(one);
myArrList.add(two);
ArrayList<String> yourArrList = myArrList;
one.replace("O", "B");
for (String val : myArrList)
    System.out.print(val + ":");
for (String val : yourArrList)
    System.out.print(val + ":");
  1. One:Two:One:Two:

  2. Bne:Two:Bne:Two:

  3. One:Two:Bne:Two:

  4. Bne:Two:One:Two:

清除 ArrayList 元素

你可以通过调用clear来移除ArrayList中的所有元素。以下是一个示例:

ArrayList<String> myArrList = new ArrayList<String>();
myArrList.add("One");
myArrList.add("Two");
myArrList.clear();
for (String val:myArrList)
    System.out.println(val);

之前的代码不会打印任何内容,因为myArrList中没有更多元素。

访问单个 ArrayList 元素

在本节中,我们将介绍以下用于访问ArrayList元素的ArrayList方法:

  • get(int index)—此方法返回此列表中指定位置的元素。

  • size()—此方法返回此列表中的元素数量。

  • contains(Object o)—如果此列表包含指定的元素,则此方法返回true

  • indexOf(Object o)—此方法返回此列表中指定元素首次出现的索引,如果此列表不包含该元素,则返回–1

  • lastIndexOf(Object o)—此方法返回此列表中指定元素最后一次出现的索引,如果此列表不包含该元素,则返回–1

你可以通过以下方式检索ArrayList中特定位置的元素并确定其大小:

在幕后,get方法将通过与数组大小进行比较来检查请求的位置是否存在于ArrayList中。如果请求的元素不在范围内,get方法将在运行时抛出java.lang.IndexOutOfBoundsException错误。

剩下的所有三个方法—containsindexOflastIndexOf—要求你对如何确定对象相等性有一个明确且深入的理解。ArrayList存储对象,这三个方法将比较你传递给这些方法的值与ArrayList中所有元素的值。

默认情况下,如果对象被相同的变量引用(String 类是一个例外,它有自己的 String 对象池),则认为这些对象是相等的。如果您想根据对象的状态(实例变量的值)来比较对象,请在该类中重写 equals 方法。我已经在 4.4.5 节 中通过在 MyPerson 类中重写 equals 方法,展示了如何确定一个类中对象的相等性,以及当类重写了它的 equals 方法时和没有重写时的情况。

让我们看看所有这些方法的用法:

之前代码的输出如下:

false
true
-1
1
-1
2

查看使用重写了 equals 方法的 MyPerson 对象列表的相同代码的输出。首先,这是类 MyPerson 的定义:

MiscMethodsArrayList4 的定义如下:

如前述代码的输出所示,MyPerson 类中对象的相等性是由其 equals 方法中定义的规则确定的。具有相同 name 实例变量值的两个 MyPerson 类对象被认为是相等的。myArrList 存储了 MyPerson 类的对象。为了找到目标对象,myArrList 将依赖于 MyPerson 类的 equals 方法给出的输出;它不会比较存储对象和目标对象的引用。

考试技巧

ArrayList 可以存储重复的对象值。

克隆 ArrayList

ArrayList 中定义的 clone 方法返回这个 ArrayList 实例的 浅拷贝。“浅拷贝”意味着此方法创建了一个要克隆的 ArrayList 对象的新实例。它的元素引用被复制,但对象本身并没有被复制。

这里有一个例子:

让我们回顾一下之前的代码:

  • myArrList 引用的对象赋值给 assignedArrList。现在变量 myArrListassignedArrList 引用了相同的对象。

  • myArrList 引用的对象的副本赋值给 clonedArrList。变量 myArrListclonedArrList 引用了不同的对象。因为 clone 方法返回的是 Object 类型的值,所以它被转换为 ArrayList<String-Builder> 类型以赋值给 clonedArrList(如果您不理解这一行,不要担心——类型转换将在 第六章 中介绍)。

  • 输出 true,因为 myArrListassignedArrList 引用了相同的对象。

  • 输出 false,因为 myArrListclonedArrList 引用了不同的对象,因为 clone 方法创建并返回一个新的 ArrayList 对象(但具有相同的列表成员)。

  • 证明了 clone 方法没有复制 myArrList 的元素。所有变量引用 myArrValAssignedArrValclonedArrVal 都指向相同的对象。

  • 因此, 都会打印 true

从 ArrayList 创建数组

您可以使用 toArray 方法返回一个包含 ArrayList 中所有元素的数组,从第一个元素到最后一个元素。如本章前面所述(参见图 4.27[#ch04fig27] 在 4.4.1 节中),ArrayList 使用一个私有变量 elementData(一个数组)来存储其自己的值。toArray 方法不返回对这个数组的引用。它创建一个新的数组,将 ArrayList 的元素复制到其中,然后返回它。

现在是棘手的部分。ArrayList 不保留对返回数组的引用,该数组本身也是一个对象。但是,对ArrayList中各个元素的引用被复制到返回数组中,并且仍然由ArrayList引用。

这意味着,如果您通过交换元素位置或为其元素分配新对象等方式修改返回数组,ArrayList 的元素将不会受到影响。但是,如果您修改返回数组(可变)元素的状态,则修改后的元素状态将在 ArrayList 中反映出来。

4.5. 比较对象是否相等

[3.2] 使用 == 和 equals() 测试字符串和其他对象之间的相等性

在 4.1 节 中,您看到了类 String 如何定义一组规则来确定两个 String 值是否相等,以及这些规则如何在 equals 方法中编码。同样,任何 Java 类都可以定义一组规则来确定其两个对象是否应该被视为相等。这种比较是通过 equals 方法完成的,该方法将在下一节中描述。

4.5.1. 类 java.lang.Object 中的 equals 方法

方法 equals 定义在类 java.lang.Object 中。所有 Java 类都直接或间接继承了这个类。列表 4.2 包含了类 java.lang.Object 中方法 equals 的默认实现。

列表 4.2. 类 java.lang.Objectequals 方法的实现
public boolean equals(Object obj) {
    return (this == obj);
}

如您所见,equals 方法的默认实现仅比较两个对象变量是否引用了同一个对象。因为实例变量用于存储对象的状态,所以通常比较实例变量的值来确定两个对象是否应该被视为相等。

4.5.2. 比较用户定义类的对象

让我们以类 BankAccount 的一个示例来工作,该类定义了两个实例变量:acctNumber 类型为 StringacctType 类型为 intequals 方法比较这些实例变量的值以确定 BankAccount 类的两个对象是否相等。

这里是相关的代码:

让我们在以下代码中验证这个 equals 方法的功能:

输出 false,因为引用变量 b1b2 的值不匹配。 输出 true,因为引用变量 b2b3 的值彼此匹配。String 类型的对象传递给在 Bank-Account 类中定义的 equals 方法。如果传递给它的方法参数不是 BankAccount 类型,该方法返回 false。因此, 输出 false

即使以下实现对于实际使用的类来说不可接受,它仍然是语法正确的:

class BankAccount {
    String acctNumber;
    int acctType;
    public boolean equals(Object anObject) {
        return true;
    }
}

之前定义的 equals 方法对于与 BankAccount 类的对象进行比较的任何对象都会返回 true,因为它不比较任何值。让我们看看当您使用 equals() 方法比较 String 类型的对象与 BankAccount 类型的对象以及反之亦然时会发生什么:

在前面的代码中, 输出 true,但 输出 falseString 类中的 equals 方法仅在比较的对象是一个具有相同字符序列的 String 时返回 true

考试提示

在考试中,请注意关于正确实现 equals 方法的题目(参考第 4.5.4 节),以比较两个对象,而不是关于简单编译正确的 equals 方法的题目。如果您被问及之前示例代码中的 equals() 是否可以正确编译,正确答案应该是可以。

4.5.3. equals 方法的错误方法签名

编写接受自身类实例的 equals 方法是一个常见的错误。在下面的代码中,BankAccount 类没有重写 equals(),而是重载了它:

虽然之前的 equals() 定义看起来似乎是完美的,但当您尝试将 BankAccount 类型的对象(如前述代码所示)添加到 ArrayList 并检索时会发生什么?ArrayList 类中定义的 contains 方法通过调用对象的 equals 方法来比较两个对象。它不比较对象引用。

在以下代码中,看看当您将 Bank-Account 类型的对象添加到 ArrayList 中,然后尝试验证列表是否包含与正在搜索的对象具有相同 acctNumberacctType 实例变量值的 BankAccount 对象时会发生什么:

定义了具有相同状态的 BankAccount 类的 b1b2 对象。b1 添加到列表中。 将对象 b2 与列表中添加的对象进行比较。

一个ArrayList使用equals方法来比较两个对象。因为BankAccount类没有遵循正确定义(重写)equals方法的规则,所以ArrayList使用基类Object中的equals方法,该方法比较对象引用。因为代码没有将b2添加到list中,所以它打印出false

你认为如果改变BankAccount类中equals方法的定义,使其接受类型为Object的方法参数,之前的代码会输出什么?自己试一试!

考试技巧

equals方法定义了一个类型为Object的方法参数,其返回类型为boolean。当你定义(重写)此方法以比较两个对象时,不要更改方法名、返回类型或方法参数的类型。

4.5.4. equals方法的约定

Java API 定义了equals方法的约定,当你在任何类中实现它时应该注意。以下合同说明直接来自 Java API 文档:^([1])

¹

Java API 文档中关于equals方法的说明可以在 Oracle 网站上找到:docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals(java.lang.Object.

equals方法在非null对象引用上实现了一个等价关系:

  • 它是自反的:对于任何非null引用值xx.equals(x)应该返回true

  • 它是对称的:对于任何非null引用值xy,如果y.equals(x)返回true,则x.equals(y)也应该返回true

  • 它是传递的:对于任何非null引用值xyz,如果x.equals(y)返回truey.equals(z)返回true,则x.equals(z)应该返回true

  • 它是一致的:对于任何非null引用值xy,多次调用x.equals(y)应始终返回true或始终返回false,前提是在对象上用于equals()比较的信息没有被修改。

  • 对于任何非null引用值xx.equals(null)应该返回false

根据约定,我们在前面的示例中为BankAccount类定义的equals方法的定义违反了equals方法的约定。再次查看定义:

public boolean equals(Object anObject) {
    return true;
}

即使将null值传递给此方法,此代码也会返回true。根据equals方法的约定,如果将null值传递给equals方法,则该方法应返回false

考试技巧

你可能会回答关于 equals 方法契约的明确问题。如果一个 equals 方法对其传入的 null 对象返回 true,则违反了契约。此外,如果 equals 方法修改了传递给它的方法参数的任何实例变量的值,或者修改了它被调用的对象上的值,它将违反 equals 契约。

hashCode() 方法

许多程序员对方法 hashCode 在确定对象相等性中的作用感到困惑。hashCode 方法不是由 equals 方法调用来确定两个对象的相等性的。因为 hashCode 方法不在考试范围内,所以我将在这里简要讨论它,以消除对这种方法的任何混淆。

hashCode 方法由存储 - 对的集合类(如 HashMap)使用,其中 是一个对象。这些集合类使用 hashCode 来高效地搜索相应的 (一个对象)的 hashCode 用于指定一个 号,该 应该存储其相应的 。两个 不同 对象的 hashCode 值可以相同。当这些集合类找到正确的 时,它们将调用 equals 方法来选择正确的 对象(具有相同的 值)。即使 中只有一个对象,也会调用 equals 方法。毕竟,可能会有相同的哈希值但不同的 equals,而且没有匹配项可以获取!

根据 Java 文档,当你在你自己的类中重写 equals 方法时,你也应该重写 hashCode 方法。如果你不这样做,如果你的类作为存储 - 对的集合类(如 HashMap)的 使用,它们的行为将不会如预期。这个方法在本章中没有详细讨论,因为它不在考试范围内。但在你的实际项目中,不要忘记使用 equals 方法重写它。

4.6. 处理日历数据

[9.3] 使用 java.time.LocalDateTimejava.time.LocalDatejava.time.LocalTimejava.time.format.DateTimeFormatterjava.time.Period 类创建和操作日历数据

Java 8 中的新日期和时间 API 简化了日期和时间对象的处理。它包括具有简单和有信息性的方法名称的类和接口。当你在本节中使用 LocalDateLocalTimeLocalDateTimePeriodDateTimeFormatter 类时,你会注意到这些类定义了具有相似名称(具有相似目的)的方法。表 4.1 列出了方法前缀、其类型及其用途(来自 Oracle Java 文档)。

表 4.1. Java 8 日期和时间 API 中的方法前缀、类型及其用途
前缀 方法类型 用途
of static 创建一个实例,其中工厂主要验证输入参数,而不是转换它们。
from static 将输入参数转换为目标类的实例,这可能涉及从输入中丢失信息。
parse static 将输入字符串解析为目标类的实例。
format instance 使用指定的格式化程序将时间对象中的值格式化为字符串。
get instance 返回目标对象状态的一部分。
is instance 查询目标对象的状态。
with instance 返回一个副本的目标对象,其中一个元素已更改;这是 JavaBean 上 set 方法的不可变等效。
plus instance 返回添加了时间量的目标对象的一个副本。
minus instance 返回从目标对象中减去时间量后的副本。
to instance 将此对象转换为另一种类型。
at instance 将此对象与另一个对象组合。
注意

前面的表格可能在此点看起来没有增加太多价值。但随着您进入以下部分,您将意识到在日期和时间类中定义的方法名称的相似性。

让我们从LocalDate类开始。

4.6.1. LocalDate

要存储像生日或周年纪念日、参观某个地方或开始工作、学校或大学这样的日期,您不需要存储时间。在这种情况下,LocalDate将完美工作。

LocalDate可以用来存储没有时间或时区的日期,如 2015-12-27。LocalDate实例是不可变的,因此在多线程环境中使用是安全的。

创建LocalDate

LocalDate构造函数被标记为私有,因此您必须使用其中一个工厂方法来实例化它。静态方法of()接受年、月和月份:

注意

在 Java 8 中引入的新日期和时间 API 中,1 月由int类型的值1表示,而不是0。基于旧日历的 API 在 Java 8 中没有改变,仍然使用基于 0 的月份编号。

要从系统时钟获取当前日期,请使用静态方法now()

LocalDate date3 =  LocalDate.now();

您还可以解析格式为 2016-02-27 的字符串来实例化LocalDate。以下是一个示例:

LocalDate date2 = LocalDate.parse("2025-08-09");
考试提示

如果您向parse()of()传递无效值,您将得到DateTimeParseException。传递给parse()的字符串格式必须正好是 9999-99-99 的格式。传递给parse()的月份和日期值必须是两位数字;单个数字被视为无效值。对于值为 1-9 的天和月,传递 01-09。

查询LocalDate

您可以使用实例方法如getXX()来查询LocalDate的年、月和日期值。日期可以查询为月份中的日、周中的日和年中的日。月份值可以检索为枚举常量Monthint值:

LocalDate date = LocalDate.parse("2020-08-30");
System.out.println(date.getDayOfMonth());
System.out.println(date.getDayOfWeek());
System.out.println(date.getDayOfYear());
System.out.println(date.getMonth());
System.out.println(date.getMonthValue());
System.out.println(date.getYear());

前面代码的输出如下所示:

30
SUNDAY
243
AUGUST
8
2020

您可以使用实例方法 isAfter()isBefore() 来确定一个日期是否在时间上早于或晚于另一个日期:

操作 LocalDate

LocalDate 类定义了名为 minusXX()plusXX()withXX() 的方法来操作日期值。API 架构师使用了使它们的目的明确化的名称。因为 LocalDate 是一个不可变类,所以所有的方法都会创建并返回一个副本。minusXX() 方法返回一个副本的日期,从其中减去指定的天数、月份或年份:

LocalDate bday = LocalDate.of(2052,03,10);
System.out.println(bday.minusDays(10));
System.out.println(bday.minusMonths(2));
System.out.println(bday.minusWeeks(30));
System.out.println(bday.minusYears(1));

这里是前面代码的输出:

2052-02-29
2052-01-10
2051-08-13
2051-03-10
考试提示

LocalDate 是不可变的。所有看似操作其值的方法都会返回它被调用的 LocalDate 实例的副本。

plusXX() 方法在向日期实例添加指定的天数、月份或年份后返回日期实例的副本:

LocalDate launchCompany = LocalDate.of(2016,02,29);
System.out.println(launchCompany.plusDays(1));
System.out.println(launchCompany.plusMonths(1));
System.out.println(launchCompany.plusWeeks(7));
System.out.println(launchCompany.plusYears(1));

这里是前面代码的输出:

2016-03-01
2016-03-29
2016-04-18
2017-02-28
考试提示

所有对 LocalDate 的添加、减法或替换都考虑闰年。

withXX() 方法返回日期实例的副本,替换其中的指定日、月或年:

LocalDate firstSex = LocalDate.of(2036,02,28);
System.out.println(firstSex.withDayOfMonth(1));
System.out.println(firstSex.withDayOfYear(1));
System.out.println(firstSex.withMonth(7));
System.out.println(firstSex.withYear(1));

前面代码的输出如下所示:

2036-02-01
2036-01-01
2036-07-28
0001-02-28
转换为其他类型

LocalDate 类定义了将其转换为 LocalDateTimelong(表示纪元日期)的方法。

LocalDate 类定义了重载的 atTime() 实例方法。这些方法将 LocalDate 与时间结合,创建并返回 LocalDateTime,它存储日期和时间(LocalDateTime 类将在下一节中介绍):

LocalDate interviewDate = LocalDate.of(2016,02,28);
System.out.println(interviewDate.atTime(16, 30));
System.out.println(interviewDate.atTime(16, 30, 20));
System.out.println(interviewDate.atTime(16, 30, 20, 300));
System.out.println(interviewDate.atTime(LocalTime.of(16, 30)));

这里是前面代码的输出:

2016-02-28T16:30
2016-02-28T16:30:20
2016-02-28T16:30:20.000000300
2016-02-28T16:30
考试提示

如果您将任何无效的小时、分钟或秒值传递给 atTime 方法,它将在运行时抛出 DateTimeException

您可以使用 toEpochDay() 方法将 LocalDate 转换为 纪元日期——从 1970 年 1 月 1 日起的天数计数:

LocalDate launchBook = LocalDate.of(2016,2,8);
LocalDate aDate = LocalDate.of(1970,1,8);
System.out.println(launchBook.toEpochDay());
System.out.println(aDate.toEpochDay());

这里是前面代码的输出:

16839
7
注意

在标准日期和时间中,纪元日期指的是 1970 年 1 月 1 日 00:00:00 GMT。

4.6.2. LocalTime

要存储像早餐、会议演讲开始时间或在店销售结束时间这样的时间,您可以使用 LocalTime。它以小时-分钟-秒(不带时区)的格式存储时间,并精确到纳秒。因此,您肯定会看到接受纳秒作为方法参数的方法或返回此值的方法。像 LocalDate 一样,LocalTime 也是不可变的,因此在多线程环境中使用是安全的。

创建 LocalTime

LocalTime 构造函数是私有的,因此您必须使用其中一个工厂方法来实例化它。静态方法 of() 接受小时、分钟、秒和纳秒:

of()方法使用 24 小时制来指定小时值。如果您向of()传递超出范围的小时、分钟或秒值,会发生什么?在这种情况下,您将得到一个运行时异常,DateTimeException。如果传递给方法的值范围不符合方法的参数类型,您将得到编译错误。以下是一个示例:

考试技巧

LocalTime没有定义用于传递上午或下午的方法。使用 0–23 的值来定义小时。如果您向小时、分钟或秒传递超出范围的值,您将得到一个运行时异常。

要从系统时钟获取当前时间,请使用静态方法now()

LocalDate date3 =  LocalTime.now();

您可以使用LocalTime的静态方法parse()将字符串解析为LocalTime实例。您可以选择传递格式为 15:08:23(小时:分钟:秒)的字符串,或者使用DateTimeFormatter(下一节介绍)解析任何文本:

LocalTime time = LocalTime.parse("15:08:23");
考试技巧

如果您向parse()传递无效的字符串值,代码将编译但会抛出运行时异常。如果您没有传递DateTimeFormatter,传递给parse()的字符串格式必须正好是 99:99:99 的格式。传递给parse()的小时和分钟值必须是两位数字;单个数字被视为无效值。对于值为 0–9 的小时和分钟,传递 00–09。

使用本地时间常量

您可以使用LocalTime类中的常量来处理预定义的时间:

  • LocalTime.MIN—支持的最小时间,即 00:00

  • LocalTime.MAX—支持的最大时间,即 23:59:59.999999999

  • LocalTime.MIDNIGHT—一天开始的时间,即 00:00

  • LocalTime.NOON—中午时间,即 12:00

这是否让你想知道最小时间和午夜时间是否相等?亲自看看吧;以下代码输出true

查询本地时间

您可以使用类似getXX()的实例方法查询LocalTime的小时、分钟、秒和纳秒。所有这些方法都返回一个int值:

LocalTime time = LocalTime.of(16, 20, 12, 98547);
System.out.println(time.getHour());
System.out.println(time.getMinute());
System.out.println(time.getSecond());
System.out.println(time.getNano());

这里是输出结果:

16
20
12
98547
考试技巧

查询LocalTime的正确方法名是get-Hour()getMinute()getSecond()getNano()。注意考试中可能会使用无效的方法名,如getHours(), getMinutes(), getSeconds(),getNanoSeconds()

您可以使用实例方法isAfter()isBefore()来检查时间是否在指定时间之后或之前。以下代码输出true

操作本地时间

你可以使用实例方法minusHours()minusMinutes()minusSeconds()minusNanos()来创建并返回具有指定时间段减去的LocalTime实例的副本。方法名称是自解释的。例如,minus-Hours(int)返回一个在指定时间段内减去小时的LocalTime实例的副本。以下示例计算并输出 Shreya 应该离开办公室去看电影的时间,假设电影在 21:00 小时开始,通勤到电影院需要 35 分钟:

LocalTime movieStartTime = LocalTime.parse("21:00:00");
int commuteMin = 35;
LocalTime shreyaStartTime = movieStartTime.minusMinutes(commuteMin);
System.out.println("Start by " + shreyaStartTime + " from office");

以下是前述代码的输出:

Start by 20:25 from office
考试技巧

getXXX()方法不同,minusXXX()方法使用复数形式:getHour()minusHours()getMinute()minusMinutes()getSecond()minusSeconds(),以及getNano()minusNanos()

plusHours()plusMinutes()plusSeconds()plusNanos()方法接受long值,并将指定的小时、分钟、秒或纳秒添加到时间中,返回其副本作为LocalTime。以下示例使用addSeconds()isAfter()方法向时间添加秒,并将其与另一个时间进行比较:

int worldRecord = 10;
LocalTime raceStartTime = LocalTime.of(8, 10, 55);
LocalTime raceEndTime = LocalTime.of(8, 11, 11);
if (raceStartTime.plusSeconds(worldRecord).isAfter(raceEndTime))
    System.out.println("New world record");
else
    System.out.println("Try harder");

前述代码的输出如下:

Try harder
考试技巧

LocalTime是不可变的。对其实例调用任何方法都不会修改其值。

withHour()withMinute()withSecond()withNano()方法接受一个int值,并返回一个具有指定值更改的LocalTime副本。以下示例创建了一个新的LocalTime实例,其分钟值为00

LocalTime startTime = LocalTime.of(5, 7, 9);
if (startTime.getMinute() < 30)
    startTime = startTime.withMinute(0);
System.out.println(startTime);

以下是输出:

05:00:09
与其他类型的组合

LocalTime类定义了atDate()方法,用于将LocalDate与自身结合以创建LocalDateTime

LocalTime time = LocalTime.of(14, 10, 0);
LocalDate date = LocalDate.of(2016,02,28);
LocalDateTime dateTime = time.atDate(date);
System.out.println(dateTime);

以下是输出:

2016-02-28T14:10
考试技巧

LocalTime类定义了atDate()方法,该方法可以传入一个LocalDate实例来创建一个LocalDateTime实例。

4.6.3. LocalDateTime

如果你想存储日期和时间(不含时区),请使用LocalDateTime类。它存储的值类似于2050-06-18T14:20:30:908765(年-月-日T小时:分钟:秒:纳秒)。

注意

LocalDateTime类使用字母T在其打印值中分隔日期和时间值。

你可以将此类视为LocalDateLocal-Time类功能的结合。此类定义了与LocalDateLocalTime类中定义的类似方法。因此,而不是讨论此类中单个方法,以下是一个涵盖此类重要方法的示例:

在下一节中,你将了解如何使用Period类进行日期和时间的计算。

4.6.4. Period

人们经常谈论年、月或日的周期。使用 Java 8 Date API,你可以使用 Period 类来这样做。Period 类表示基于日期的年、月和日的时间量,如 2 年、5 个月和 10 天。要处理基于秒和纳秒的时间量,你可以使用 Duration 类。

注意

Duration 类可以用来存储像 1 小时、36 分钟或 29.4 秒这样的时间量。但这个类在本考试(和本书)中并没有明确介绍。它包含在 OCP Java SE 8 Programmer II 考试中。

你可以从 LocalDateLocalDateTime 类中添加或减去 Period 实例。Period 也是一个不可变类,因此在多线程环境中使用是安全的。让我们通过实例化 Period 开始吧。

实例化 Period

使用私有构造函数,Period 类定义了多个工厂方法来创建其实例。静态方法 of()ofYears()ofMonths()ofWeeks()ofDays() 接受 int 值来创建年、月、周或日的周期:

考试提示

35 天的周期不是存储为 1 个月和 5 天。它的各个元素,即日、月和年,是以初始化的方式存储的。

你也可以通过向所有前面的方法传递负整数来定义负数周期。以下是一个快速示例:

考试提示

你可以定义正数或负数的时间周期。例如,你可以定义代表 15 或 -15 天的 Period 实例。

你也可以使用其静态方法 parse 通过解析字符串来实例化 Period。此方法解析格式为 PnYnMnDPnW 的字符串值,其中 n 代表一个数字,而字母 (P, Y, M, D, 和 W) 分别代表解析、年、月、日和周。这些字母可以是大写或小写。每个字符串必须以字母 pP 开头,并且必须包含至少四个部分之一,即年、月、周或日。对于字符串格式 PnW,周的数量乘以 7 以得到天数。你还可以使用 parse() 定义负数周期。如果你在传递给 parse() 的完整字符串值之前加上负号 (-),则它应用于所有值。如果你在单个数字之前放置一个负号,则它仅应用于该部分。以下是一些实例化五年周期的示例(注意大小写字母和 + 和 – 符号的使用):

以下示例定义了不同持续时间的周期:

Period p5Yrs7 = Period.parse("P5y1m2d");
Period p5Yrs8 = Period.parse("p9m");
Period p5Yrs9 = Period.parse("P60d");
Period p5Yrs10 = Period.parse("-P5W");

当传递给 System.out.println() 时,前面代码中的变量将产生以下输出:

P5Y1M2D
P9M
P60D
P-35D
考试提示

如果你向 parse() 传递无效的字符串值,代码将编译但会抛出运行时异常。

你也可以使用静态方法 between(LocalDate dateInclusive, LocalDate dateExclusive) 来实例化 Period

考试提示

静态方法between接受两个LocalDate实例,并返回一个表示两个日期之间年、日和月数的Period实例。返回的Period中包含第一个日期,但不包含第二个日期。这里有一个快速记住它的方法:周期 = 结束日期 - 开始日期。

使用周期操作LocalDateLocalDateTime

在日常生活中,从日期中添加或减去天数、月数或年数是很常见的。Period类实现了TemporalAmount接口,因此可以使用LocalDateTimeLocalDate类中定义的plus()minus()方法。以下示例向LocalDate实例添加一个周期:

LocalDate date = LocalDate.of(2052, 01, 31);
System.out.println(date.plus(Period.ofDays(1)));

以下是上述代码的输出:

2052-02-01

当您向任何年份的 1 月 31 日添加一个周期时会发生什么?您会得到 2 月的最后一天还是 3 月的第一天?以下示例向LocalDateTime实例添加一个周期:

LocalDateTime dateTime = LocalDateTime.parse("2052-01-31T14:18:36");
System.out.println(dateTime.plus(Period.ofMonths(1)));

上述代码的输出如下所示:

2052-02-29T14:18:36
考试技巧

因为Period实例可以表示正数或负数周期(如 15 天或-15 天),所以您可以通过调用plus方法从LocalDateLocalDateTime中减去天数。

类似地,您可以使用minus()方法与LocalDateLocalDateTime类一起使用,以减去年、月、周或天数:

LocalDateTime dateTime = LocalDateTime.parse("2020-01-31T14:18:36");
System.out.println(dateTime.minus(Period.ofYears(2)));

LocalDate date = LocalDate.of(2052, 01, 31);
System.out.println(date.minus(Period.ofWeeks(4)));

这里是输出:

2018-01-31T14:18:36
2052-01-03
查询周期实例

您可以使用实例方法getYears()getMonths()getDays()来查询Period实例的年、月和日。所有这些方法都返回一个int值:

Period period = Period.of(2,4,40);
System.out.println(period.getYears());
System.out.println(period.getMonths());
System.out.println(period.getDays());

上述代码输出以下内容:

2
4
40
考试技巧

当您使用超过 31 天的天数或超过 12 个月的月份初始化Period实例时,它不会重新计算其年、月或日组件-。

您可以使用isNegativeisZero方法查询一个Period周期的三个单位中是否有任何一个为负。如果三个单位都为零,则Period实例为零。isNegative方法如果其三个组件中至少有一个是严格负数(<0)则返回true

操作周期

您可以使用实例方法minus(TemporalAmount)minusDays(long)minusWeeks(long)minusMonths(long)minusYears(long)multipliedBy(int)来创建并返回具有指定周期减去或修改的Period实例的副本。方法名称是自解释的。例如,minusDays(long)返回一个从指定天数减去的Period实例的副本。您可以使用以下示例在 10 天后向您的朋友发送提醒(限于打印消息)以提醒一个事件,比如生日庆祝活动:

LocalDate bday = LocalDate.of(2020, 10, 29);
LocalDate today = LocalDate.now();
Period period10Days = Period.of(0, 0, 10);

if (bday.minus(period10Days).isBefore(today))
    System.out.println("Time to send out reminders to friends");
考试技巧

Period类中,getXXX()方法和minusXXX()方法都使用复数形式:getYears()minusHours()

当你从一个代表一个月的 PeriodP1M)减去一个代表 10 天的 PeriodP10D)时会发生什么?你会得到一个代表 20 天的 Period 或一个代表 -1 个月和 10 天的 Period?让我们通过以下代码来找出答案,该代码还包括所有 minusXXX 方法的快速示例代码:

Period period10Days = Period.of(0, 0, 10);
Period period1Month = Period.of(0, 1, 0);

System.out.println(period10Days.minus(period1Month));
System.out.println(period10Days.minusDays(5));
System.out.println(period10Days.minusMonths(5));
System.out.println(period10Days.minusYears(5));

这里是输出:

P-1M10D
P5D
P-5M10D
P-5Y10D
考试技巧

当使用 minusXXX() 方法减去 Period 实例时,将分别减去各个元素。从 P1M 减去 P10D 返回 P1M-10D,而不是 P20D

Period 类定义了 multipliedBy(int) 方法,该方法将周期中的每个元素乘以整数值:

考试技巧

Period 类中的 multipliedBy(int) 方法用于修改 Period 实例的所有元素。Period 没有定义 “divideBy()” 方法。getXXX() 方法和 minusXXX() 方法都使用复数形式 getYears()minusHours()

plus(TemporalAmount)plusDays(long)plusWeeks(long)plusMonths(long)plusYears(long) 方法将 Period 实例添加到 Period 实例中,并返回修改后的值作为 Period。与 minusXXX() 方法一样,所有 plusXXX() 方法都添加单个元素:

Period period5Month = Period.of(0, 5, 0);
Period period10Month = Period.of(0, 10, 0);
Period period10Days = Period.of(0, 0, 10);

System.out.println(period5Month.plus(period10Month));
System.out.println(period10Days.plusDays(35));
System.out.println(period10Days.plusMonths(5));
System.out.println(period10Days.plusYears(5));

上述代码的输出如下:

P15M
P45D
P5M10D
P5Y10D
考试技巧

将 10 个月的 Period 加到 5 个月的 Period 上得到 15 个月,而不是 1 年和 3 个月。

withDays()withMonths()withYears() 方法接受一个 int 值,并返回一个具有指定值更改的 Period 的副本。

转换到其他类型

toTotalMonths() 方法通过将年数乘以 12 并加上月数来返回周期中的总月数:

Period 可以作为 LocalDate 单参数 plus()minus() 方法的参数。当你想要给一个特定的日期加上 3 个月和 10 天时会发生什么?每年月份数量是固定的,但每月天数不是。查看 LocalDate 源代码中的 plus()minus() 方法可以看到,年份被转换为月份,并且总是先处理月份再处理天数。

在下一节中,你将使用 DateTimeFormatter 类。

4.6.5. DateTimeFormatter

定义在 java.time.format 包中的 DateTimeFormatter 类可以用来格式化和解析日期和时间对象。在本节中,你将使用此类使用预定义的常量(如 ISO_LOCAL_DATE)、使用模式(如 yyyy-MM-dd)或本地化样式(如 longshort)来格式化或解析日期和时间对象。

格式化或解析日期或时间对象的第一步是访问 DateTime-Formatter,然后对日期或时间对象或 DateTimeFormatter 调用 formatparse 方法。让我们详细探讨这些步骤,从多种方式实例化或访问 DateTimeFormatter 对象开始。

实例化或访问 DateTimeFormatter

你可以通过多种方式实例化或访问 DateTimeFormatter 对象:

  • 通过调用静态 ofXXX 方法,并传递一个 FormatStyle

  • 通过访问 DateTimeFormatter 的公共静态字段

  • 通过使用静态方法 ofPattern 并传递一个字符串值

从第一个选项开始,你可以通过调用其 ofXXX 静态方法并传递一个 FormatStyle 值(FormatStyle.FULLFormatStyle.LONGFormatStyle.MEDIUMFormatStyle.SHORT)来实例化一个 DateTimeFormatter 以处理日期、时间或日期/时间对象:

DateTimeFormatter formatter1 =
                  DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
DateTimeFormatter formatter2 =
                  DateTimeFormatter.ofLocalizedTime(FormatStyle.FULL);
DateTimeFormatter formatter3 =
                  DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
DateTimeFormatter formatter4 =
                  DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT,
                                                        FormatStyle.SHORT);
注意

ofLocalizedDateofLocalizedTimeofLocalizedDateTime 方法根据代码执行的系统上的区域设置(语言、区域或国家)来格式化日期和时间对象。因此,输出可能会在不同系统之间略有差异。

表 4.2 展示了使用不同的 Format-Style 值如何格式化日期或时间对象。

表 4.2. FormatStyle 对日期(例如,2057 年 8 月 11 日)或时间(例如,14 小时 30 分钟和 15 秒)对象格式化影响的示例
FormatStyle 示例
FormatStyle.FULL Saturday, August 11, 2057
FormatStyle.LONG August 11, 2057
FormatStyle.MEDIUM Aug 11, 2057
FormatStyle.SHORT 8/11/57
FormatStyle.FULL
FormatStyle.LONG
FormatStyle.MEDIUM 2:30:15 PM
FormatStyle.SHORT 2:30 PM

你可以通过使用该类的公共和静态字段来访问 DateTimeFormatter 对象:

DateTimeFormatter formatter5 = DateTimeFormatter.ISO_DATE;

表 4.3 列出了与本次考试相关的几个预定义格式化工具。

表 4.3. DateTimeFormatter 类中的预定义格式化工具及其如何格式化日期(例如,2057 年 8 月 11 日)或时间(例如,14 小时 30 分钟和 15 秒)对象的示例
预定义格式化工具 示例
BASIC_ISO_DATE 20570811
ISO_DATE/ISO_LOCAL_DATE 2057-08-11
ISO_TIME/ISO_LOCAL_TIME 14:30:15.312
ISO_DATE_TIME/ISO_LOCAL_DATE_TIME 2057-08-11T14:30:15.312

你可以通过使用静态方法 ofPattern 并传递一个字符串值来使用模式(字母和符号)实例化一个 DateTimeFormatter

DateTimeFormatter formatter6= DateTimeFormatter.ofPattern("yyyy MM dd");

你可以使用前面的代码将日期格式化为 2057 08 11。表 4.4(Table 4.4)列出了可以用来定义此类模式的字母。

表 4.4. 用于定义 DateTimeFormatter 模式的字母及其如何格式化日期(例如,2057 年 8 月 11 日)或时间(例如,14 小时 30 分钟和 15 秒)对象的示例
符号 含义 示例
y, Y 2057; 57
M 年中的月份 8; 08; Aug; August
D 年内天数 223
d 月份中的天数 11
E 星期几 Sat
e 本地化星期几 7; Sat
a 一天中的上午或下午 pm
h 上午或下午的时钟小时数 03
H 一天中的小时数 14
m 小时中的分钟数 30
s 分钟的秒数 15
' 文本转义
考试技巧

DateTimeFormatter 可以定义规则来格式化或解析日期对象、时间对象或两者。

使用 DateTimeFormatter 格式化日期或时间对象

要格式化日期或时间对象,您可以使用日期/时间对象的实例 format 方法或 DateTimeFormatter 类的实例 format 方法。在幕后,日期和时间对象的 format 方法只是调用 DateTimeFormatterformat 方法。表 4.5 列出了可用的 format 方法。

注意

TemporalAccessor 是一个接口,由 LocalDateLocalTimeLocalDateTime 类实现。在考试中不会明确考察这个接口。

表 4.5. LocalDateLocalTimeLocalDateTimeDateTimeFormatter 类中的 format 方法
定义于 返回类型 方法签名和描述
LocalDate String format(DateTimeFormatter) 使用指定的 DateTimeFormatter 格式化此日期对象
LocalTime String format(DateTimeFormatter) 使用指定的 DateTimeFormatter 格式化此时间对象
LocalDateTime String format(DateTimeFormatter) 使用指定的 DateTimeFormatter 格式化此日期/时间对象
DateTimeFormatter String format(TemporalAccessor) 使用此格式化器格式化日期/时间对象
考试技巧

注意传入实例方法 format 的参数数量和类型。当在 LocalDateLocalTimeLocalDateTime 实例上调用 format 时,将 DateTimeFormatter 实例作为方法参数传递。当在 DateTimeFormatter 上调用 format 时,将 LocalDateLocalTimeLocalDateTime 实例作为方法参数传递。

DateTimeFormatter 中的 format 方法使用格式器的规则将日期或时间对象格式化为 String。以下示例使用 FormatStyle 风格(风格在 表 4.2 中列出)格式化 LocalDate 对象:

如果在前面代码中将时间对象(LocalTime)而不是日期对象(LocalDate)传递给 format 方法,会发生什么?它能否编译或成功执行(更改以粗体显示)?

前面的代码可以成功编译,但不会执行。它将抛出一个运行时异常,因为 formatter 定义了格式化日期对象的规则(使用 ofLocalizedDate() 创建),但其 format() 方法接收了一个时间对象。

考试技巧

如果将日期对象传递给定义了格式化时间对象规则的 DateTimeFormatter 实例上的 format 方法,它将抛出一个运行时异常。

使用 DateTimeFormatter 格式化日期和时间对象(这些对象是通过字符串模式创建的)很有趣(但也令人困惑)。注意在模式中使用的字母的大小写。MmDd 是不同的。此外,使用模式字母并不指定数字或文本的数量。例如,使用 YYYYY 格式化日期对象会返回相同的结果。以下是一些使用不同模式的示例:

LocalDate date = LocalDate.of(2057,8,11);
LocalTime time = LocalTime.of(14,30,15);

DateTimeFormatter d1 = DateTimeFormatter.ofPattern("y");
DateTimeFormatter d2 = DateTimeFormatter.ofPattern("YYYY");
DateTimeFormatter d3 = DateTimeFormatter.ofPattern("Y M D");
DateTimeFormatter d4 = DateTimeFormatter.ofPattern("e");

DateTimeFormatter t1 = DateTimeFormatter.ofPattern("H h m s");
DateTimeFormatter t2 = DateTimeFormatter.ofPattern("'Time now:'HH mm a");

System.out.println(d1.format(date));
System.out.println(d2.format(date));
System.out.println(d3.format(date));
System.out.println(d4.format(date));

System.out.println(t1.format(time));
System.out.println(t2.format(time));

以下是前面代码的输出:

2057
2057
2057 8 223
7
14 2 30 15
Time now:14 30 PM
考试技巧

如果你分不清 MmDd,请记住大写字母代表更长的持续时间。所以 M 代表月份,m 代表分钟。同样,D 代表一年中的某一天;d 代表一个月中的某一天。

你也可以通过在日期或时间对象上调用 format 方法并将 DateTimeFormatter 实例传递给它来格式化日期和时间对象。

备注

如果你访问 Java 的源代码,你会注意到日期和时间类中的 formatparse 方法只是简单地调用 DateTimeFormatter 实例上的 formatparse 方法。

使用 DateTimeFormatter 解析日期或时间对象

要解析日期或时间对象,你可以使用日期/时间对象中的静态 parse 方法或 DateTimeFormatter 类中的实例 parse 方法。在幕后,日期/时间对象中的 parse 方法只是调用 DateTimeFormatter 中的 parse 方法。表 4.6 列出了可用的解析方法。

考试技巧

解析方法在 LocalDateLocalTimeLocalDateTime 类中定义为静态方法。DateTimeFormatter 类将 parse 方法定义为实例方法。

表 4.6. LocalDateLocalTimeLocalDateTimeDateTimeFormatter 类中的 parse 方法
定义在 返回类型 方法签名和描述
LocalDate LocalDate parse(CharSequence) 使用文本字符串(如 2057-08-11)创建 LocalDate 实例,使用 DateTimeFormatter.ISO_LOCAL_DATE 解析
LocalDate LocalDate parse(CharSequence, DateTimeFormatter) 使用指定的格式器解析文本创建 LocalDate 实例
LocalTime LocalTime parse(CharSequence) 使用文本字符串(如 14:40)创建 LocalTime 实例,使用 Date-TimeFormatter.ISO_LOCAL_TIME 解析
LocalTime LocalTime parse(CharSequence, DateTimeFormatter) 使用指定的格式器解析文本创建 LocalTime 实例
LocalDateTime LocalDateTime parse(CharSequence) 使用文本字符串(如 2057-08-11T14:40)创建 LocalDateTime 实例,使用 DateTimeFormatter.ISO_LOCAL_DATE_TIME 解析
LocalDateTime LocalDateTime parse(CharSequence, DateTimeFormatter) 使用指定的格式器解析文本创建 LocalDateTime 实例
DateTimeFormatter TemporalAccessor parse(CharSequence) 使用 DateTimeFormatter 的规则解析文本,返回一个时间对象
考试技巧

当在 LocalDateLocalTimeLocalDateTime 实例上调用 parse 方法时,你可能没有指定格式化程序。在这种情况下,分别使用 DateTimeFormatter.ISO_LOCAL_DATEDateTimeFormatter.ISO_LOCAL_TIMEDateTimeFormatter.ISO_LOCAL_DATE_TIME 来解析文本。

让我们使用 LocalDateLocalTimeLocalDateTimeparse 方法,通过使用 DateTimeFormatter 解析字符串值,生成日期或时间对象:

DateTimeFormatter d1 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse("2057-01-29", d1 );

以下行抛出 DateTimeParseException,因为此机制仅在所有组件都存在的情况下才有效。例如,使用“2057-01-29”的 yyyy-MM-dd 模式运行良好。组件顺序无关紧要;因此,使用 dd-yyyy-MM 来解析“29-2057-01”也有效,并得到 2057 年 1 月 29 日:

DateTimeFormatter d1 = DateTimeFormatter.ofPattern("yyyy");
LocalDate date = LocalDate.parse("2057", d1);

同样,你可以调用 parse 方法来创建 LocalTimeLocalDateTime 的实例。

4.7. 摘要

在本章中,你学习了 String 类、其属性和方法。由于这是 Java 中使用最频繁的类之一,我将重申,对这一类在特定方法行为原因的理解将大大有助于你成功完成 OCA Java SE 8 程序员 I 考试。

你学习了如何使用操作符 new 和赋值操作符(=)以及 String 文字来初始化 String 变量。你还学习了使用这两种方法存储 String 对象之间的区别。如果你使用赋值操作符来初始化你的 String 变量,它们将存储在一个公共的 String 对象池中(也称为 String 常量池),可以被其他人使用。这种存储是可能的,因为 String 对象是不可变的——也就是说,它们的值不能被更改。

你学习了 char 数组如何用于存储 String 对象的值。这有助于解释为什么 charAt()indexOf()substring() 方法在位置 0 而不是位置 1 处搜索 String 的第一个字符。我们还回顾了 replace()trim()substring() 方法,这些方法似乎会修改 String 的值,但永远不会这样做,因为 String 对象是不可变的。你还学习了 length()startsWith()endsWith() 方法。

由于并非所有操作符都可以与 String 一起使用,你学习了可以与 String 一起使用的操作符:++===!=。你还学习了 String 的相等性可以使用 equals 方法确定。通过使用操作符 ==,你只能确定两个变量是否都指向同一个对象;它不比较 String 存储的值。与其他所有对象类型一样,你可以将 null 赋值给 String 变量。

您曾使用过StringBuilder类,该类定义在java.lang包中,用于存储可变字符序列。StringBuilder类通常用于存储需要经常修改的字符序列——例如,当您在为数据库应用程序构建查询时。与String类一样,StringBuilder也使用char数组来存储其字符。StringBuilder类中定义的许多方法与String类中定义的方法完全相同,例如charAtindexOfsubstringlength方法。append方法用于向StringBuilder对象的末尾添加字符。insert方法是另一个重要的StringBuilder方法,用于在StringBuilder对象中指定位置插入单个或多个字符。StringBuilder类提供了与StringBuffer类相同的功能,但不需要同步的方法提供了额外的特性。

数组是一个存储值集合的对象。数组可以存储原始数据类型的集合或对象的集合。您可以定义一维和多维数组。一维数组是一个指向标量值集合的对象。二维(或更多)数组被称为多维数组。二维数组指的是一个对象集合,其中每个对象都是一个一维数组。同样,三维数组指的是二维数组的集合,依此类推。数组可以一次性或分步声明、分配和初始化。二维数组不需要对称,并且它的每一行可以定义不同数量的成员。您可以定义原始数据类型、接口、抽象类和具体类的数组。所有数组都是对象,并且可以访问从java.lang.Object类继承的变量length和方法。

ArrayList 是一个可调整大小的数组,它提供了数组和 List 数据结构提供的最佳功能组合。您可以使用 add 方法向 ArrayList 中添加对象。您可以通过使用增强型 for 循环或使用 get() 方法或迭代器来访问 ArrayList 中的对象。ArrayList 保留其元素的插入顺序。ListIteratorIterator 和增强型 for 循环将按它们被添加到 ArrayList 中的顺序返回元素。您可以使用 set 方法修改 ArrayList 的元素。您可以通过使用 remove 方法移除 ArrayList 的元素,该方法接受元素位置或对象。您还可以使用 addAll 方法向 ArrayList 中添加多个元素。ArrayList 类中定义的 clone 方法返回此 ArrayList 实例的浅拷贝。"浅拷贝"意味着该方法创建了一个新的 ArrayList 实例以进行克隆,但 ArrayList 元素没有被复制。

您可以通过重写 equals 方法来比较您类中的对象。equals 方法在 java.lang.Object 类中定义,这是 Java 中所有类的基类。equals 方法的默认实现仅比较对象引用是否相等。因为实例变量用于存储对象的状态,所以在 equals 方法中比较这些变量的值以确定两个对象是否应该被视为相等是很常见的。Java API 文档为 equals 方法定义了一个契约。在考试中,对于 equals 方法的给定定义,重要的是要注意编译成功、编译失败以及不遵循契约的 equals 方法的区别。

Java 8 的日期和时间 API 简化了您与日期和时间类的工作方式。您使用了 LocalDate,它用于存储仅日期的格式 2016-08-14. LocalTime 以 14:09:65:23(小时:分钟:秒:纳秒)的格式存储时间。LocalDateTime 类存储日期和时间。Period 类用于处理,例如,4 个月或 4 天的持续时间。DateTimeFormatter 类用于使用预定义或自定义格式格式化日期和时间。

4.8. 复习笔记

本节列出了本章涵盖的主要要点。

String 类:

  • String 类表示一个不可变的字符序列。

  • 您可以使用 new 运算符或使用 String 文字值与赋值运算符来初始化 String 变量。

  • 使用不带 new 运算符的 String 文字创建的 String 对象被放置在一个 String 对象的 中。每当 JRE 收到使用赋值运算符初始化 String 变量的新请求时,它会检查是否已存在具有相同值的 String 对象。如果找到了,它将从池中返回现有 String 对象的引用。

  • 使用 new 运算符创建的 String 对象永远不会放置在 String 对象的池中。

  • 比较运算符 (==) 比较的是 String 引用,而 equals 方法比较的是 String 值。

  • String 中定义的任何方法都不能修改其值。

  • charAt(int index) 方法可以检索 String 中指定索引处的字符。

  • indexOf 方法可以用来在 String 中搜索一个 charString 的出现,从第一个位置或指定的位置开始搜索。

  • substring 方法可以用来检索 String 对象的一部分。substring 方法不包括结束位置处的字符。

  • trim 方法将返回一个新的 String,通过从 String 中移除所有前导和尾随空白字符来实现。此方法不会移除 String 中的任何空白字符。

  • 您可以使用 length 方法来获取 String 的长度。

  • startsWith 方法确定一个 String 是否以指定的 String 开头。

  • endsWith 方法确定一个 String 是否以指定的 String 结尾。

  • 在单行代码中使用多个 String 方法是一种常见的做法。当方法链式调用时,方法是从左到右评估的。

  • 您可以使用连接运算符 ++= 以及比较运算符 !===String 对象一起使用。

  • Java 语言通过使用运算符 ++= 提供了对连接 String 对象的特殊支持。

  • 比较两个 String 值是否相等的方法是使用在 String 类中定义的 equals 方法。如果被比较的对象不是 null,并且是一个表示与被比较对象相同字符序列的 String 对象,则此方法返回 true 值。

  • 比较运算符 == 判断两个引用变量是否都指向同一个 String 对象。因此,它不是比较 String 值的正确运算符。

StringBuilder

  • StringBuilder 定义在包 java.lang 中,表示一个可变的字符序列。

  • 当用户需要经常修改字符序列时,StringBuilder 类非常高效。因为它可变,所以不需要创建一个新的 StringBuilder 对象就可以修改 StringBuilder 对象的值。

  • 可以使用其构造函数创建一个 StringBuilder 对象,该构造函数可以接受一个 String 对象、另一个 StringBuilder 对象、一个 int 值来指定 StringBuilder 的容量,或者不接受任何参数。

  • StringBuilder类中定义的charAtindexOfsubstringlength方法与在String类中定义的同名方法工作方式相同。

  • append方法将指定的值添加到StringBuilder对象现有值的末尾。

  • insert方法允许您在StringBuilder对象的指定位置插入字符。appendinsert方法之间的主要区别在于,insert方法允许您在特定位置插入所需数据,而append方法只允许您在StringBuilder对象的末尾添加所需数据。

  • delete方法从指定的StringBuilder的子字符串中删除字符。deleteCharAt方法删除指定位置的char

  • String类不同,StringBuilder类没有定义trim方法。

  • reverse方法反转StringBuilder中字符的顺序。

  • StringBuilder类中的replace方法通过字符的位置替换一个由其位置确定的字符序列,用另一个String替换。

  • 除了使用substring方法外,您还可以使用subSequence方法检索StringBuilder对象的子序列。

数组:

  • 数组是一个存储值集合的对象。

  • 数组本身是一个对象。

  • 数组可以存储两种类型的数据——原始数据类型的集合和对象的集合。

  • 您可以定义一维和多维数组。

  • 一维数组是一个引用集合标量值的对象。

  • 二维(或更多)数组被称为多维数组。

  • 二维数组指的是一个对象集合,其中每个对象都是一个一维数组。

  • 类似地,三维数组指的是二维数组的集合,依此类推。

  • 多维数组可能或可能不包含每行或每列相同数量的元素。

  • 创建数组涉及三个步骤:数组的声明、数组的分配和数组元素的初始化。

  • 数组声明由数组类型、变量名和一个或多个[]的出现组成。

  • 方括号可以跟在变量名或其类型之后。在多维数组的情况下,它可以跟在两者之后。

  • 数组声明创建了一个引用null的变量。

  • 由于在声明数组时没有创建数组元素,因此不能在声明中定义数组的大小。

  • 数组分配为数组的元素分配内存。当你为数组分配内存时,你必须指定其维度,例如数组应存储的元素数量。

  • 因为数组是一个对象,所以它使用new关键字分配,后面跟存储值的类型,然后是其大小。

  • 一旦分配,所有数组元素都存储其默认值。存储对象的数组元素引用 null。存储原始数据类型的数组元素存储 0(对于整型 byteshortintlong),0.0(对于十进制类型 floatdouble),false(对于 boolean),或 /u0000(对于 char 数据)。

  • 要访问二维数组中的元素,使用两个数组位置值。

  • 你可以将数组声明、分配和初始化的所有步骤合并为单步操作。

  • 当你在单步中组合数组声明、分配和初始化时,你不能指定数组的大小。数组的大小通过分配给数组的值的数量来计算。

  • 你可以声明和分配一个数组,但可以选择不初始化其元素(例如,int[] a = new int[5];)。

  • Java 编译器不会检查你尝试访问数组元素时索引位置的范围。如果请求的索引位置在运行时不属于有效范围,代码将抛出 ArrayIndexOutOfBounds-Exception 异常。

  • 多维数组可以是非对称的;它可能或可能不为每一行定义相同数量的列。

  • 数组的类型也可以是 interfaceabstract 类。这样的数组可以用来存储从 interface 类型或 abstract 类类型继承的类的对象。

  • 数组的类型也可以是 java.lang.Object。因为所有类都扩展了 java.lang.Object 类,所以这个数组的元素可以引用任何对象。

  • 所有数组都是对象,并且可以访问变量 length,该变量指定数组存储的组件数量。

  • 因为所有数组都是对象,所以它们继承并可以访问 Object 类的所有方法。

ArrayList

  • ArrayList 是集合框架中最广泛使用的类之一。它提供了数组和列表数据结构提供的最佳功能组合。

  • ArrayList 类似于可调整大小的数组。

  • 与数组不同,你无法指定初始大小来创建 ArrayList

  • ArrayList 实现了 List 接口,并允许添加 null 值。

  • ArrayList 实现了所有列表操作(addmodifydelete 值)。

  • ArrayList 允许添加重复值,并保持其插入顺序。

  • 你可以使用 IteratorListIterator 或增强型 for 循环来遍历 ArrayList 的项目。

  • ArrayList 支持泛型,使其类型安全。

  • 在内部,java.lang.Object 类型的数组用于在 ArrayList 中存储数据。

  • 你可以通过使用 add 方法在 ArrayList 的末尾或指定位置添加一个值。

  • 一个迭代器(IteratorListIterator)允许你在遍历 ArrayList 时移除元素。使用 for 循环遍历 ArrayList 时无法移除元素。

  • ArrayList保留其元素的插入顺序。ListIterator和增强型for循环将按元素添加到ArrayList中的顺序返回元素。

  • 您可以使用set方法通过替换ArrayList中的现有元素或修改其现有值来修改ArrayList

  • remove(int)从列表中移除指定位置的元素。

  • remove(Object o)从列表中移除指定的元素的第一种出现,如果存在的话。

  • 您可以使用addAll方法从另一个ArrayList或任何其他是Collection子类的类中添加多个元素到ArrayList

  • 您可以通过调用clear方法来移除所有的ArrayList元素。

  • get(int index)返回列表中指定位置的元素。

  • size()返回列表中的元素数量。

  • contains(Object o)如果列表包含指定的元素,则返回true

  • indexOf(Object o)返回列表中指定元素第一次出现的索引,如果列表不包含该元素则返回-1

  • lastIndexOf(Object o)返回列表中指定元素最后一次出现的索引,如果列表不包含该元素则返回-1

  • ArrayList类中定义的clone方法返回此ArrayList实例的浅拷贝。浅拷贝意味着该方法创建了一个新的ArrayList实例以进行克隆,但ArrayList元素没有被复制。

  • 您可以使用toArray方法返回一个包含ArrayList中所有元素的数组,从第一个元素到最后一个元素按顺序排列。

比较对象以确定相等性:

  • 任何 Java 类都可以定义一组规则来确定两个对象是否应该被视为相等。

  • equals方法定义在java.lang.Object类中。所有 Java 类直接或间接继承了这个类。

  • equals方法的默认实现仅检查两个对象变量是否引用同一个对象。

  • 因为实例变量用于存储对象的状态,所以通常比较实例变量的值来确定两个对象是否应该被视为相等。

  • 当您在类中重写equals方法时,请确保您使用正确的equals方法签名。

  • Java API 为equals方法定义了一个约定,您在实现任何类中的方法时都应该注意。

  • 根据方法equals的约定,如果向它传递了一个null值,则方法equals应该返回false

  • 如果equals方法修改了传递给它的方法参数的任何实例变量的值,或者修改了它所调用的对象上的值,它将违反约定。

LocalDate:

  • LocalDate可用于存储没有时间或时区的日期,如 2015-12-27。

  • LocalDate实例是不可变的。

  • LocalDate构造函数被标记为私有。

  • 使用 LocalDate 的重载静态方法 of() 来实例化它:

    • public static LocalDate of(int year, int month, int dayOfMonth)

    • public static LocalDate of(int year, Month month, int dayOfMonth)

  • 当传递给 of() 方法的值超出范围时,of() 方法将抛出 DateTimeException

  • 在 Java 8 中发布的日期类中,1 月用 int1 表示,而不是 0。Java 7 或之前定义的日期类使用 0 来表示 1 月。

  • LocalDate 的静态方法 now() 返回系统时钟的当前日期,作为一个 LocalDate 实例。

  • 使用 LocalDate 的静态方法 parse() 解析格式为 2016-02-27 的字符串以实例化 LocalDate

  • 如果将无效值传递给 parse()of(),您将得到 DateTimeParseException。传递给 parse() 的字符串格式必须正好是 9999-99-99 的格式。传递给 parse() 的月份和日期值必须是两位数字;单个数字被视为无效值。对于值为 1–9 的日和月,传递 01–09。

  • 您可以使用 LocalDate 的实例方法 getXX() 来查询 LocalDate 的年、月和日期值:

    • getDayOfMonth()

    • getDayOfWeek()

    • getDayOfYear()

    • getMonth()

    • getMonthValue()

    • getYear()

  • LocalDate 的实例 minusXX() 方法返回从其值中减去指定天数、月份、周或年份后的副本:

    • minusDays()

    • minusMonths()

    • minusWeeks()

    • minusYears()

  • LocalDate 是不可变的。所有看似操纵其值的方法都返回调用它的 LocalDate 实例的副本。

  • plusXX() 方法返回在它上添加了指定天数、月份或年份后的 LocalDate 值的副本:

    • plusDays()

    • plusMonths()

    • plusWeeks()

    • plusYears()

  • withXX() 方法返回 LocalDate 值的副本,其中替换了指定的日、月或年:

    • withDayOfMonth()

    • withDayOfYear()

    • withMonth()

    • withYear()

  • 所有对 LocalDate 的添加、减去或替换都考虑闰年。

  • 尽管前述方法(添加减去替换)使用了动词,但它们实际上并没有修改现有的 LocalDate——所有这些方法都返回一个应用了请求更改的新实例。

  • LocalDate 类定义了重载的 atTime() 实例方法。这些方法将 LocalDate 与时间结合,创建并返回 LocalDateTime,它存储日期和时间。

  • 使用 toEpochDay() 方法将 LocalDate 转换为纪元日期——从 1970 年 1 月 1 日起的天数计数。

LocalTime:

  • 它以小时-分钟-秒(不带时区)的格式存储时间。

  • 它以纳秒精度存储时间。

  • LocalTime 是不可变的。

  • 您可以使用 LocalTime 的静态方法 of() 来实例化 LocalTime,该方法接受小时、分钟、秒和纳秒。

  • of() 方法使用 24 小时制来指定小时值。

  • 如果您向 of() 方法传递无效的范围值,它将抛出一个运行时异常,DateTimeException

  • LocalTime 没有定义传递上午或下午的方法。使用 0–23 的值来定义小时。如果您传递超出范围的值给小时、分钟或秒,您将得到一个运行时异常。

  • 要从系统时钟获取当前时间,请使用静态方法 now()

  • 您可以使用其静态方法 parse() 将字符串解析为 LocalTime 实例。您可以选择传递格式为 15:08:23(小时:分钟:秒)的字符串,或者使用 DateTimeFormatter 解析任何文本。

  • 如果您向 parse() 传递无效的字符串值,代码将编译但会抛出一个运行时异常。如果您没有传递 DateTimeFormatter,传递给 parse() 的字符串的格式必须正好是 99:99:99 的格式。传递给 parse() 的小时和分钟值必须是两位数;单个数字被视为无效值。对于值为 0–9 的小时和分钟,传递 00–09。

  • 您可以使用 LocalTime 类的常量来处理预定义的时间:

    • LocalTime.MIN—支持的最小时间,即 00:00

    • LocalTime.MAX—支持的最大时间,即 23:59:59.999999999

    • LocalTime.MIDNIGHT—一天开始的时间,即 00:00

    • LocalTime.NOON—中午时间,即 12:00

  • 您可以使用类似 getXX() 的实例方法来查询 LocalTime 的时、分、秒和纳秒。所有这些方法都返回一个 int 值。

  • 查询 LocalTime 的正确方法名是 getHour()getMinute()getSecond()getNano()。注意考试中可能使用无效方法名的情况,如 getHours()getMinutes()getSeconds()getNanoSeconds()

  • 您可以使用实例方法 isAfter()isBefore() 来检查时间是否在指定时间之后或之前。

  • 您可以使用实例方法 minusHours()minusMinutes()minusSeconds()minusNanos() 来创建并返回具有指定时间段减去的 LocalTime 实例的副本。

  • getXXX() 方法不同,minusXXX() 方法使用复数形式:getHour() 对比 minusHours()getMinute() 对比 minusMinutes()getSecond() 对比 minusSeconds()getNano() 对比 minusNanos()

  • plusHours()plusMinutes()plusSeconds()plusNanos() 方法接受长整型值,并将指定的小时、分钟、秒或纳秒添加到时间中,返回其副本作为 LocalTime

  • LocalTime 是不可变的。对实例调用任何方法都不会修改其值。

  • withHour()withMinute()withSecond()withNano() 方法接受一个 int 值,并返回一个具有指定值更改的 LocalTime 副本。

  • LocalTime 定义了 atDate() 方法,该方法可以传入一个 Local-Date 实例来创建一个 LocalDateTime 实例。

LocalDateTime:

  • LocalDateTime 存储一个值,如 2050-06-18T14:20:30:908765(年-月-日 Th 时:分:秒:纳秒)。

  • LocalDateTime 类在其打印值中使用字母 T 来分隔日期和时间值。

  • 你可以将此类视为提供 LocalDateLocalTime 类的功能。此类定义了与 LocalDateLocalTime 类中定义的方法类似的方法。

Period

  • Period 类表示基于日期的年、月和日的时间量,例如 2 年、5 个月和 10 天。要处理基于秒和纳秒的时间量,你可以使用 Duration 类。

  • 你可以从 LocalDateLocalDateTime 类中添加或减去 Period 实例。

  • Period 是一个不可变类。

  • Period 类定义了多个工厂方法来创建其实例。静态方法 of()ofYears()ofMonths()ofWeeks()ofDays() 接受 int 值来创建年、月、周或日的时间周期。

  • 35 天的 Period 不是存储为 1 个月和 5 天。它的各个元素,即天数、月份和年份,是以其初始化的方式存储的。

  • 你可以定义正数或负数的时间周期。你可以定义表示 15 或 -15 天的 Period 实例。

  • 你还可以使用其静态方法 parse 通过解析字符串来实例化 Period。此方法解析格式为 PnYnMnD 或 PnW 的字符串值,其中 n 代表一个数字,而字母 (P, Y, M, D, 和 W) 分别代表解析、年、月、日和周。这些字母可以是小写或大写。每个字符串必须以字母 pP 开头,并且必须包含至少四个部分之一,即年、月、周或日。

  • 如果你向 parse() 方法传递无效的字符串值,代码将编译但会抛出运行时异常。

  • 你还可以使用静态方法 between(LocalDate dateInclusive, LocalDate dateExclusive) 来实例化 Period

  • 静态方法 between 接受两个 LocalDate 实例,并返回一个表示两个日期之间年、日和月数的 Period 实例。第一个日期包含在内,但第二个日期在返回的 Period 中被排除。

  • Period 类实现了 TemporalAmount 接口,因此它可以与 LocalDateTimeLocalDate 类中定义的 plus()minus() 方法一起使用。

  • 因为 Period 实例可以表示正数或负数周期(例如 15 天或 -15 天),你可以通过调用 plus 方法从 LocalDateLocalDateTime 中减去天数。

  • 类似地,你可以使用 minus() 方法与 LocalDateLocalDateTime 类一起使用,以减去年、月、周或日的时间周期。

  • 你可以使用实例方法 getYears()getMonths()getDays() 来查询 Period 实例的年、月和日。所有这些方法都返回一个 int 值。

  • 当你使用超过 31 天或超过 12 个月的日期初始化 Period 实例时,它不会重新计算其年、月或日组件。

  • 你可以使用 isNegativeisZero 方法查询 Period 的三个单位中是否有任何一个为负。如果 Period 的所有三个单位都是零,则 Period 实例为负。

  • 你可以使用实例方法 minus(TemporalAmount)minusDays(long)minus-Months(long)minusYears(long)multipliedBy(int) 来创建并返回具有指定周期减去或修改的 Period 实例的副本。

  • Period 类中,getXXX() 方法和 minusXXX() 方法都使用复数形式:getYears()minusHours()

  • 当使用 minusXXX() 方法减去 Period 实例时,其各个元素会被减去。从 P1M 减去 P10D 返回 P1M-10D,而不是 P20D。

  • Period 类中的 multipliedBy(int) 方法用于修改 Period 实例的所有元素。Period 没有定义 divideBy

  • 将 10 个月的 Period 加到 5 个月的 Period 上得到 15 个月,而不是 1 年和 3 个月。

  • toTotalMonths() 方法通过将年数乘以 12 并加上月数来返回周期中的总月数。

DateTimeFormatter

  • 定义在 java.time.format 包中,DateTimeFormatter 类可以用来格式化和解析日期和时间对象。

  • DateTimeFormatter 可以定义规则来格式化或解析日期对象、时间对象或两者。

  • 你可以通过多种方式实例化或访问 DateTimeFormatter 对象:

    • 通过调用静态 ofXXX 方法,传递一个 FormatStyle

    • 通过访问 DateTimeFormatter 的公共静态字段

    • 通过使用静态方法 ofPattern 并传递一个字符串值

  • 使用 ofXXX 方法实例化 DateTimeFormatter 时,传递一个 FormatStyle 值(FormatStyle.FULLFormatStyle.LONGFormatStyle.MEDIUMFormatStyle.SHORT)。

  • 你可以通过使用这个类的公共和静态字段来访问 DateTimeFormatter 对象:BASIC_ISO_DATEISO_DATEISO_TIMEISO_DATE_TIME

  • DateTimeFormatter 中的 format 方法使用格式器的规则将日期或时间对象格式化为 String

  • 要解析日期或时间对象,你可以使用日期/时间对象中的 parse 方法或 DateTimeFormatter 类中的 parse 方法。

4.9. 样本考试问题

Q4-1.

以下代码的输出是什么?

class EJavaGuruArray {
    public static void main(String args[]) {
        int[] arr = new int[5];
        byte b = 4; char c = 'c'; long longVar = 10;
        arr[0] = b;
        arr[1] = c;
        arr[3] = longVar;
        System.out.println(arr[0] + arr[1] + arr[2] + arr[3]);
    }
}
  1. 4c010
  2. 4c10
  3. 113
  4. 103
  5. 编译错误

Q4-2.

以下代码的输出是什么?

class EJavaGuruArray2 {
    public static void main(String args[]) {
        int[] arr1;
        int[] arr2 = new int[3];
        char[] arr3 = {'a', 'b'};
        arr1 = arr2;
        arr1 = arr3;
        System.out.println(arr1[0] + ":" + arr1[1]);
    }
}
  1. 0:0
  2. a:b
  3. 0:b
  4. a:0
  5. 编译错误

Q4-3.

以下哪些是定义多维 int 数组的有效代码行?

  1. int[][] array1 = {{1, 2, 3}, {}, {1, 2,3, 4, 5}};
  2. int[][] array2 = new array() {{1, 2, 3}, {}, {1, 2,3, 4, 5}};
  3. int[][] array3 = {1, 2, 3}, {0}, {1, 2,3, 4, 5};
  4. int[][] array4 = new int[2][];

Q4-4.

以下哪些陈述是正确的?

  1. 以下代码执行没有错误或异常:

  2. ArrayList<Long> lst = new ArrayList<>();
    lst.add(10);
    
  3. 因为ArrayList只存储对象,所以你不能将ArrayList的元素传递给switch构造。

  4. ArrayList上调用clear()remove()将移除其所有元素。

  5. 如果你经常向ArrayList添加元素,指定更大的容量将提高代码效率。

  6. ArrayList上调用方法clone()创建其浅拷贝;也就是说,它不会克隆单个列表元素。

Q4-5.

以下哪个陈述是正确的?

  1. ArrayList提供了一个可调整大小的数组,你可以通过它提供的方法轻松管理。你可以从ArrayList中添加和删除元素。
  2. ArrayList存储的值可以被修改。
  3. 你可以使用for循环、IteratorListIterator遍历ArrayList的元素。
  4. 在你可以在ArrayList中存储任何元素之前,ArrayList要求你指定元素的总数。
  5. ArrayList可以存储任何类型的对象。

Q4-6.

以下代码的输出是什么?

import java.util.*;                                             // line 1
class EJavaGuruArrayList {                                      // line 2
    public static void main(String args[]) {                    // line 3
        ArrayList<String> ejg = new ArrayList<>();              // line 4
        ejg.add("One");                                         // line 5
        ejg.add("Two");                                         // line 6
        System.out.println(ejg.contains(new String("One")));    // line 7
        System.out.println(ejg.indexOf("Two"));                 // line 8
        ejg.clear();                                            // line 9
        System.out.println(ejg);                                // line 10

        System.out.println(ejg.get(1));                         // line 11
    }                                                           // line 12
}                                                               // line 13
  1. 第 7 行打印true
  2. 第 7 行打印false
  3. 第 8 行打印-1
  4. 第 8 行打印1
  5. 第 9 行移除列表ejg的所有元素。
  6. 第 9 行将列表ejg设置为null
  7. 第 10 行打印null
  8. 第 10 行打印[]
  9. 第 10 行打印一个类似于ArrayList@16356的值。
  10. 第 11 行抛出异常。
  11. 第 11 行打印null

Q4-7.

以下代码的输出是什么?

class EJavaGuruString {
    public static void main(String args[]) {
        String ejg1 = new String("E Java");
        String ejg2 = new String("E Java");
        String ejg3 = "E Java";
        String ejg4 = "E Java";
        do
            System.out.println(ejg1.equals(ejg2));
        while (ejg3 == ejg4);
    }
}
  1. 打印一次true
  2. 打印一次false
  3. 在无限循环中打印true
  4. 在无限循环中打印false

Q4-8.

以下代码的输出是什么?

class EJavaGuruString2 {
    public static void main(String args[]) {
        String ejg = "game".replace('a', 'Z').trim().concat("Aa");
        ejg.substring(0, 2);
        System.out.println(ejg);
    }
}
  1. gZmeAZ
  2. gZmeAa
  3. gZm
  4. gZ
  5. game

Q4-9.

以下代码的输出是什么?

class EJavaGuruString2 {
    public static void main(String args[]) {
        String ejg = "game";
        ejg.replace('a', 'Z').trim().concat("Aa");
        ejg.substring(0, 2);
        System.out.println(ejg);
    }
}
  1. gZmeAZ
  2. gZmeAa
  3. gZm
  4. gZ
  5. game

Q4-10.

以下代码的输出是什么?

class EJavaGuruStringBuilder {
    public static void main(String args[]) {
        StringBuilder ejg = new StringBuilder(10 + 2 + "SUN" + 4 + 5);
        ejg.append(ejg.delete(3, 6));
        System.out.println(ejg);
    }
}
  1. 12S512S5
  2. 12S12S
  3. 1025102S
  4. 运行时异常

Q4-11.

以下代码的输出是什么?

class EJavaGuruStringBuilder2 {
    public static void main(String args[]) {
        StringBuilder sb1 = new StringBuilder("123456");
        sb1.subSequence(2, 4);
        sb1.deleteCharAt(3);
        sb1.reverse();
        System.out.println(sb1);
    }
}
  1. 521
  2. 运行时异常
  3. 65321
  4. 65431

Q4-12.

以下代码的输出是什么?

String printDate = LocalDate.parse("2057-08-11")
                      .format(DateTimeFormatter.ISO_DATE_TIME);
System.out.println(printDate);
  1. August 11, 2057T00:00
  2. Saturday Aug 11,2057T00:00
  3. 08-11-2057T00:00:00
  4. 编译错误
  5. 运行时异常

4.10. 样本考试题目的答案

Q4-1.

以下代码的输出是什么?

class EJavaGuruArray {
    public static void main(String args[]) {
        int[] arr = new int[5];
        byte b = 4; char c = 'c'; long longVar = 10;
        arr[0] = b;
        arr[1] = c;
        arr[3] = longVar;
        System.out.println(arr[0] + arr[1] + arr[2] + arr[3]);
    }
}
  1. 4c010
  2. 4c10
  3. 113
  4. 103
  5. 编译错误

答案:e

解释:这个问题中的代码不会编译,因为

arr[3] = longVar;

上一行代码尝试将类型为long的值赋给类型为int的变量。因为 Java 不支持隐式缩窄转换(例如,在这个例子中,longint),赋值失败。此外,此代码试图欺骗你对以下内容的理解:

  • char值赋给int数组元素(arr[1] = c
  • byte值添加到int数组元素(arr[0] = b
  • 未分配的int数组元素是否分配了默认值(arr[2]
  • arr[0] + arr[1] + arr[2] + arr[3]打印所有这些值的总和还是连接后的值

在回答 OCA Java SE 8 Java 程序员 I 考试中的问题时,请注意这些策略。如果任何答案选项列出了编译错误或运行时异常,寻找可能导致这些错误的明显代码行。在这个例子中,arr[3] = longVar 将导致编译错误。

Q4-2.

以下代码的输出是什么?

class EJavaGuruArray2 {
    public static void main(String args[]) {
        int[] arr1;
        int[] arr2 = new int[3];
        char[] arr3 = {'a', 'b'};
        arr1 = arr2;
        arr1 = arr3;
        System.out.println(arr1[0] + ":" + arr1[1]);
    }
}
  1. 0:0
  2. a:b
  3. 0:b
  4. a:0
  5. 编译错误

答案:e

说明:因为 char 值可以赋给 int 值,你可能会认为 char 数组可以赋给 int 数组。但我们谈论的是 intchar 原始数组的数组,它们与原始的 intchar 不同。数组本身是引用变量,它指向类似类型的对象集合。

Q4-3.

以下哪些是定义多维 int 数组的有效代码行?

  1. int[][] array1 = {{1, 2, 3}, {}, {1, 2,3, 4, 5}};
  2. int[][] array2 = new array() {{1, 2, 3}, {}, {1, 2,3, 4, 5}};
  3. int[][] array3 = {1, 2, 3}, {0}, {1, 2,3, 4, 5};
  4. int[][] array4 = new int[2][];

答案:a, d

说明:选项 (b) 是错误的。此行代码无法编译,因为 new array() 不是一个有效的代码。与其它类的对象不同,数组不是使用 new 关键字后跟 array 单词来初始化的。当使用 new 关键字初始化数组时,它后面跟的是数组的类型,而不是 array 单词。

选项 (c) 是错误的。要初始化一个二维数组,所有这些值必须包含在另一对花括号内,如选项 (a) 中的代码所示。

Q4-4.

以下哪些陈述是正确的?

  1. 以下代码执行时没有错误或异常:

  2. ArrayList<Long> lst = new ArrayList<>();
    lst.add(10);
    
  3. 因为 ArrayList 只存储对象,所以你不能将 ArrayList 的元素传递给 switch 结构。

  4. ArrayList 上调用 clear()remove() 将移除其所有元素。

  5. 如果你经常向 ArrayList 添加元素,指定更大的容量将提高代码效率。

  6. ArrayList 上调用 clone() 方法会创建其浅拷贝;也就是说,它不会克隆单个列表元素。

答案:d, e

说明:选项 (a) 是错误的。非浮点数值字面量的默认类型是 int。你不能将 int 添加到 Long 类型的 ArrayList 中。你可以传递 Longlong 类型的值到其 add 方法。

选项 (b) 是错误的。从 Java 7 开始,switch 也接受 String 类型的变量。因为 String 可以存储在 ArrayList 中,所以可以在 switch 结构中使用 ArrayList 的元素。

选项 (c) 是错误的。只有 clear() 会移除 ArrayList 的所有元素。

选项(d)是正确的。ArrayList内部使用数组来存储所有元素。每次你向ArrayList添加元素时,它都会检查数组是否可以容纳新值。如果不能,ArrayList将创建一个更大的数组,将所有现有值复制到新数组中,然后将新值添加到数组的末尾。如果你经常向ArrayList添加元素,那么创建一个具有更大容量的ArrayList是有意义的,因为之前的步骤不会为每个ArrayList插入重复执行。

选项(e)是正确的。在ArrayList上调用clone()将创建一个单独的引用变量,该变量存储与要克隆的ArrayList相同数量的元素。但每个单独的ArrayList元素都将引用相同的对象;也就是说,单独的ArrayList元素没有被克隆。

Q4-5.

以下哪些陈述是正确的?

  1. ArrayList提供了一个可调整大小的数组,可以使用它提供的方法轻松管理。你可以从ArrayList中添加和删除元素
  2. ArrayList存储的值可以被修改
  3. 你可以使用for循环、IteratorListIterator遍历ArrayList中的元素
  4. ArrayList要求你在存储任何元素之前指定总元素数。
  5. ArrayList可以存储任何类型的对象

答案:a, b, c, e

说明:选项(a)是正确的。开发者可能更喜欢使用ArrayList而不是数组,因为它提供了数组和列表的所有好处。例如,你可以轻松地向ArrayList中添加或删除元素。

选项(b)是正确的。

选项(c)是正确的。ArrayList可以很容易地通过集合框架类提供的方法进行搜索、排序,并比较其值。

选项(d)是不正确的。数组要求你在添加任何元素之前指定总元素数。但你不需要在代码中的任何时间指定你可能会添加到ArrayList中的总元素数。

选项(e)是正确的。

Q4-6.

以下代码的输出是什么?

import java.util.*;                                             // line 1
class EJavaGuruArrayList {                                      // line 2
    public static void main(String args[]) {                    // line 3
        ArrayList<String> ejg = new ArrayList<>();              // line 4
        ejg.add("One");                                         // line 5
        ejg.add("Two");                                         // line 6
        System.out.println(ejg.contains(new String("One")));    // line 7
        System.out.println(ejg.indexOf("Two"));                 // line 8
        ejg.clear();                                            // line 9
        System.out.println(ejg);                                // line 10
        System.out.println(ejg.get(1));                         // line 11
    }                                                           // line 12
}                                                               // line 13
  1. 第 7 行打印true
  2. 第 7 行打印false
  3. 第 8 行打印-1
  4. 第 8 行打印1
  5. 第 9 行移除了列表ejg的所有元素
  6. 第 9 行将ejg设置为null
  7. 第 10 行打印null
  8. 第 10 行打印[]
  9. 第 10 行打印类似于ArrayList@16356的值。
  10. 第 11 行抛出异常
  11. 第 11 行打印null

答案:a, d, e, h, j

说明:第 7 行:contains方法接受一个对象并将其与列表中存储的值进行比较。如果方法找到匹配项,则返回true,否则返回false。此方法使用列表中存储的对象定义的equals方法。在示例中,ArrayList存储了String类的对象,该类已重写equals方法。String类的equals方法比较它存储的值。这就是为什么第 7 行返回值true

第 8 行:indexOf 方法在找到匹配项时返回元素的索引位置;如果没有找到匹配项,则返回 -1。此方法还使用 equals 方法在后台比较 ArrayList 中的值。因为 String 类中的 equals 方法比较其值而不是引用变量,所以 indexOf 方法在位置 1 找到匹配项。

第 9 行:clear 方法移除 ArrayList 的所有单个元素,因此尝试访问任何早期的 ArrayList 元素将抛出运行时异常。它不会将 ArrayList 引用变量设置为 null

第 10 行:ArrayList 已覆盖 toString 方法,使其返回一个包含所有元素的列表,这些元素被括号括起来。为了打印每个元素,调用 toString 方法以获取其 String 表示形式。

第 11 行:clear 方法移除 ArrayList 的所有元素。尝试访问(不存在的)ArrayList 元素会抛出运行时 IndexOutOfBounds-Exception 异常。

这个问题测试你对 ArrayList 和确定 String 对象相等性的理解。

Q4-7.

以下代码的输出是什么?

class EJavaGuruString {
    public static void main(String args[]) {
        String ejg1 = new String("E Java");
        String ejg2 = new String("E Java");
        String ejg3 = "E Java";
        String ejg4 = "E Java";
        do
           System.out.println(ejg1.equals(ejg2));
        while (ejg3 == ejg4);
    }
}
  1. true 打印一次
  2. false 打印一次
  3. true 在无限循环中打印
  4. false 在无限循环中打印

答案:c

解释:未使用 new 操作符创建的 String 对象被放置在一个 String 对象池中。因此,变量 ejg3 所引用的 String 对象被放置在一个 String 对象池中。变量 ejg4 也是未使用 new 操作符定义的。在 Java 为变量 ejg4String 池中创建另一个 String 对象之前,它会查找池中具有相同值的 String 对象。因为这个值已经在池中存在,所以它使变量 ejg4 指向同一个 String 对象。这反过来又使得变量 ejg3ejg4 指向同一个 String 对象。因此,以下两个比较都将返回 true

  • ejg3 == ejg4(比较对象引用)
  • ejg3.equals(ejg4)(比较对象值)

即使变量 ejg1ejg2 指向不同的 String 对象,但它们定义了相同的值。所以 ejg1.equals(ejg2) 也返回 true。因为循环条件 (ejg3==ejg4) 总是返回 true,所以代码在无限循环中打印 true

Q4-8.

以下代码的输出是什么?

class EJavaGuruString2 {
    public static void main(String args[]) {
        String ejg = "game".replace('a', 'Z').trim().concat("Aa");
        ejg.substring(0, 2);
        System.out.println(ejg);
    }
}
  1. gZmeAZ
  2. gZmeAa
  3. gZm
  4. gZ
  5. game

答案:b

解释:当链式调用时,方法从左到右进行评估。首先执行的是 replace 方法,而不是 concatString 对象是不可变的。在引用变量 ejg 上调用 substring 方法不会改变变量的内容。它返回一个 String 对象,该对象不被代码中的任何其他变量引用。实际上,String 类中定义的任何方法都不会修改对象的自身值。它们都创建并返回新的 String 对象。

Q4-9.

以下代码的输出是什么?

class EJavaGuruString2 {
    public static void main(String args[]) {
        String ejg = "game";
        ejg.replace('a', 'Z').trim().concat("Aa");
        ejg.substring(0, 2);
        System.out.println(ejg);
    }
}
  1. gZmeAZ
  2. gZmeAa
  3. gZm
  4. gZ
  5. game

答案:e

说明:String对象是不可变的。无论你在String对象上执行多少方法,其值都不会改变。变量ejg被初始化为String"game"。这个值不会改变,并且代码打印game

Q4-10.

以下代码的输出是什么?

class EJavaGuruStringBuilder {
    public static void main(String args[]) {
        StringBuilder ejg = new StringBuilder(10 + 2 + "SUN" + 4 + 5);
        ejg.append(ejg.delete(3, 6));
        System.out.println(ejg);
    }
}
  1. 12S512S5
  2. 12S12S
  3. 1025102S
  4. 运行时异常

答案:a

说明:这个问题测试你对运算符、StringStringBuilder的理解。以下代码行返回12SUN45

10 + 2 + "SUN" + 4 + 5

+运算符将两个数字相加,但连接最后两个数字。当+运算符遇到String对象时,它将所有剩余的操作数视为String对象。

String对象不同,StringBuilder对象是可变的。这个类中定义的appenddelete方法会改变它的值。ejg.delete(3, 6)修改了StringBuilder的现有值为12S5。然后,在调用ejg.append()时,它将相同的值追加到自身,结果值为12S512S5

Q4-11.

以下代码的输出是什么?

class EJavaGuruStringBuilder2 {
    public static void main(String args[]) {
        StringBuilder sb1 = new StringBuilder("123456");
        sb1.subSequence(2, 4);
        sb1.deleteCharAt(3);
        sb1.reverse();
        System.out.println(sb1);
    }
}
  1. 521
  2. 运行时异常
  3. 65321
  4. 65431

答案:c

说明:与substring方法一样,subSequence方法不会修改StringBuilder的内容。因此,变量sb1的值仍然是123456,即使在执行以下代码行之后:

sb1.subSequence(2, 4);

deleteCharAt方法删除位置 3 处的char值。因为位置是从 0 开始的,所以数字 4 被从值123456中删除,结果为12356reverse方法通过将其值赋给其值的反转表示来修改StringBuilder的值。12356的反转是65321

Q4-12.

以下代码的输出是什么?

String printDate = LocalDate.parse("2057-08-11")
                      .format(DateTimeFormatter.ISO_DATE_TIME);
System.out.println(printDate);
  1. August 11, 2057T00:00
  2. Saturday Aug 11,2057T00:00
  3. 08-11-2057T00:00:00
  4. 编译错误
  5. 运行时异常

答案:e

说明:这个问题中的示例代码调用了LocalDate.parse(),传递了一个字符串值但没有DateTimeFormatter实例。在这种情况下,文本2057-08-11使用DateTimeFormatter.ISO_LOCAL_DATE进行解析。LocalDate.parse()返回一个LocalDate实例。

示例代码接着在一个LocalDate实例上调用format方法,使用DateTimeFormatter.ISO_DATE_TIME。代码编译成功,因为format方法接受一个DateTimeFormatter实例。但是,在运行时抛出异常,因为它试图使用定义日期/时间对象规则的格式化器(ISO_DATE_TIME)来格式化LocalDate实例。当在LocalDate对象中找不到匹配的时间值时,会抛出异常。

考试技巧

parse 方法在 LocalDateLocalTimeLocalDateTime 类中被定义为静态方法。DateTimeFormatter 类将 parse 方法定义为实例方法。但 format() 方法则被所有类定义为实例方法。如果调用 parse()format() 的顺序颠倒,示例代码将无法编译:

String printDate = LocalDate.format(DateTimeFormatter.ISO_DATE_TIME)
                      .parse("2057-08-11");

第五章. 流程控制

本章涵盖的考试目标 你需要了解的内容
[3.3] 创建和使用 if 和 if/else 以及三元结构。 如何使用 if、if-else、if-else-if-else 和嵌套 if 结构。使用这些 if 结构时带和不带花括号{}的差异。如何使用三元结构。它与 if 和 if-else 结构相比如何。
[3.4] 使用 switch 语句。 如何通过向 switch 语句传递正确的参数类型以及 case 和 default 标签来使用 switch 语句。在 switch 语句中使用 break 和 return 语句时代码流的改变。
[5.1] 创建和使用 while 循环。 如何使用 while 循环,包括确定何时应用 while 循环。
[5.2] 创建和使用 for 循环包括增强型 for 循环。 如何使用 for 和增强型 for 循环。for 循环和增强型 for 循环的优点和缺点。可能无法使用增强型 for 循环的场景。
[5.3] 创建和使用 do/while 循环。 do/while 循环的创建和使用。每个 do/while 循环至少执行一次,即使其条件在第一次迭代时评估为 false。
[5.4] 比较循环结构。 for、增强型 for、do-while 和 while 循环之间的差异和相似之处。在给定场景或代码片段的情况下,知道哪个循环是最合适的。
[5.5] 使用 break 和 continue。 break 和 continue 语句的使用。break 语句可以在循环和 switch 语句中使用。continue 语句只能用在循环中。使用 break 或 continue 语句时代码流的差异。识别使用 break 和 continue 语句的正确场景。

我们每天都会做出多个决定,并且经常需要在多个可选项中做出选择以做出每个决定。这些决定从复杂的,比如在学校选择学习哪些科目或选择哪个职业,到简单的,比如吃什么食物或穿什么衣服。你选择的选择可能会以小或大的方式改变你的人生道路。例如,如果你选择在大学学习医学,你可能会成为一名研究科学家;如果你选择美术,你可能会成为一名画家。但决定晚餐吃意大利面还是披萨不太可能对你的人生产生重大影响。

你也可能重复执行特定的动作集合。这些动作可能从每天吃一个冰淇淋,到打电话给朋友直到接通,到在学校或大学通过考试以获得期望的学位。这些重复也可能改变你的人生道路:你可能会喜欢每天吃冰淇淋,或者享受获得更高学位带来的好处。

在 Java 中,选择语句(ifswitch)和循环语句(for、增强型forwhiledo-while)用于定义和选择不同的操作路径,以及重复代码行。你使用这些类型的语句来定义代码中的控制流程。

在 OCA Java SE 8 程序员 I 考试中,你将被问及如何定义和控制代码中的流程。为了帮助你准备,我将在本章中介绍以下主题:

  • 通过创建和使用ifif-else、三元和switch结构来选择性执行语句

  • 创建和使用循环:whiledo-whilefor和增强型for

  • 为选择和迭代语句创建嵌套结构

  • 比较 do-while、while、for 和增强型 for 循环结构

  • 使用breakcontinue语句

在 Java 中,你可以通过使用ifswitch结构有条件地执行代码。让我们从if结构开始。

5.1. ifif-else和三元结构

[3.3] 创建 if 和 if/else 以及三元结构

在本节中,我们将介绍ifif-else和三元结构。我们将检查当这些结构在有或没有花括号{}的情况下使用时会发生什么。我们还将介绍嵌套的ifif-else和三元结构。

5.1.1. if结构和其变体

if结构允许你根据条件的结果在代码中执行一组语句。这个条件必须始终评估为booleanBoolean值。你可以指定一组在条件评估为truefalse时要执行的语句。(在许多 Java 书籍中,术语constructsstatements是可互换的。)

if语句的多种形式在图 5.1 中展示:

  • if

  • if-else

  • if-else-if-else

图 5.1. if语句的多种形式:ifif-elseif-else-if-else

在图 5.1 中,condition1condition2指的是一个变量或表达式,它必须评估为booleanBoolean值;statement1、statement2statement3指的是一行代码或代码块。

考试技巧

在 Java 中,then不是一个关键字,因此不能与if语句一起使用。

让我们首先定义一组变量:scoreresultnamefile,如下所示:

int score = 100;
String result = "";
String name = "Lion";
java.io.File file = new java.io.File("F");

图 5.2 显示了ifif-elseif-else-if-else结构的用法,并通过并排显示代码来比较它们。

图 5.2. 代码中实现的if语句的多种形式

让我们快速浏览图 5.2 中的ifif-elseif-else-if-else语句所使用的代码。在下面的示例代码中,如果条件name.equals("Lion")评估为true,则将200的值赋给变量score

在以下示例中,如果条件name.equals("Lion")评估为true,则将200的值赋给变量score。如果这个条件评估为false,则将300的值赋给变量score

在以下示例中,如果score等于100,则将A的值赋给变量result。如果score等于50,则将B的值赋给变量result。如果score等于10,则将C的值赋给变量result。如果score不匹配1005010中的任何一个,则将F的值赋给变量resultif-else-if-else结构可以为所有if结构使用不同的条件:

注意

本书中的章节使用你在考试中可能看到的代码风格。考试代码风格通常包括在真实项目中不建议使用的实践,如代码缩进不当或为了简洁而省略大括号的使用。本书不鼓励你使用这种晦涩的编码实践。请编写易于阅读、理解和维护的代码。

图 5.3 说明了前面的代码,并使几个要点变得清晰:

  • 最后一个else语句是最后一个if结构的一部分,而不是它之前的任何if结构。

  • if-else-if-else是一种if-else结构,其中else部分定义了另一个if结构。一些其他编程语言,如 Python 和 Shell Script,使用elif;Perl 和 Ruby 使用elsif;VB 使用ElseIf来定义if-else-if结构。如果你使用过这些语言中的任何一种,请注意 Java 中的差异。以下代码与前面的代码等价:

    if (score == 100)
        result = "A";
    else
        if (score == 50)
            result = "B";
    
        else
            if (score == 10)
                result = "C";
            else
                result="F";
    
图 5.3. if-else-if-else代码的执行

再次注意,前面的任何if结构都没有使用then来定义如果条件评估为true时要执行的代码。与其他编程语言不同,then在 Java 中不是一个关键字,也不与if结构一起使用。

考试技巧

if-else-if-else是一种if-else结构,其中else部分定义了另一个if结构。

在用作if结构条件的boolean表达式中,也可以包含一个赋值操作。以下“故事中的转折”练习通过修改if语句比较的变量值来增加一个转折。让我们看看你是否能正确回答它(答案见附录)。

故事中的转折 5.1

按照以下方式修改前面示例中使用的代码。这段代码的输出是什么?

String result = "1";
int score = 10;
if ((score = score+10) == 100)
    result = "A";
else if ((score = score+29) == 50)
    result = "B";
else if ((score = score+200) == 10)
    result = "C";
else
     result = "F";
System.out.println(result + ":" + score);
  1. A:10

  2. C:10

  3. A:20

  4. B:29

  5. C:249

  6. F:249

5.1.2. 缺少的 else 块

如果你没有为if结构定义else语句,会发生什么?可以如下定义if结构的操作步骤(省略else部分):

boolean testValue = false;
if (testValue == true)
    System.out.println("value is true");

但你不能为if结构定义else部分,跳过if代码块。以下代码无法编译:

但在if之后跟随的空代码块可以很好地工作:

boolean testValue = false;
if (testValue == true) {}
else
    System.out.println("value is false");

这里还有另一段有趣且奇怪的代码:

是一条有效的代码行,即使它没有定义if语句的thenelse部分。在这种情况下,if条件评估,然后结束。if结构没有定义基于此条件结果应该执行的任何代码。

注意

使用if(testValue==true)与使用if(testValue)相同。同样,if(testValue==false)与使用if(!testValue)相同。本书包括这两种方法的示例。许多编程初学者发现后者(没有显式的==)的方法令人困惑。

5.1.3. 花括号{}在 if-else 结构中的存在与否的影响

if条件评估为truefalse时,你可以执行单个语句或语句块。if块通过将一个或多个语句包含在一对花括号{}内来标记。如果没有花括号,if块将执行单行代码;如果有花括号包含在块中(使用花括号定义),则可以执行无限多行代码。如果if语句中只有一行,则花括号是可选的。

以下代码仅在if语句中使用的表达式评估为true时,将值200赋给变量score

String name = "Lion";
int score = 100;
if (name.equals("Lion"))
    score = 200;

如果你想要在变量name的值等于Lion时执行另一行代码,会发生什么?以下代码是否正确?

注意

前面的代码是一个很好的例子,说明在现实世界的项目中,你必须使用花括号来编写代码,以避免错误。

如果if条件为true,则执行score = 200;语句。尽管看起来name = "Larry";语句似乎是if语句的一部分,但实际上它不是。由于缺少花括号{},它将无论if条件的结果如何都执行。

考试技巧

在考试中,要注意使用误导性缩进的if结构中的代码。如果没有定义代码块(用{}标记),则只有跟随if结构的语句将被视为其一部分。

如果你为if结构定义一个else部分,相同的代码会发生什么?

在这种情况下,代码将无法编译。编译器将报告else部分在没有if语句的情况下被定义。如果你感到困惑,请检查以下代码,其中缩进用于强调name = "Larry"行不是else结构的一部分:

如果你想要为if结构执行多个语句,请在代码块中定义它们。你可以通过在花括号{}内定义所有这些代码来实现。以下是一个示例:

同样,你也可以为else部分定义多行代码。以下示例错误地这样做:

图片

前面代码的输出如下:

Lion
Again, not a Lion

尽管代码在图片 看起来只有在变量name的值与值Lion匹配时才会执行,但这并不是事实。它缩进错误,让你误以为它是else块的一部分。前面的代码与以下代码(正确缩进)相同:

图片

如果你只想在if条件评估为false时执行前面代码中的最后两个语句,你可以通过使用{}来实现:

图片

你可以定义另一个语句、结构或循环,在if条件下执行,而不使用{},如下所示:

图片

System.out.println(i)for循环的一部分,而不是跟在for循环后面的一个无关的语句。所以这段代码是正确的,并给出以下输出:

0
1
2

5.1.4. 作为参数传递给if语句的适当与不适当表达式

if结构中使用的变量或表达式的结果必须评估为truefalse。假设以下变量的定义:

int score = 100;
boolean allow = false;
String name = "Lion";

让我们看看一些可以作为参数传递给if语句的有效变量和表达式的几个例子:

图片

使用==来比较两个String对象是错误的。如第四章中所述,比较两个String对象的正确方式是使用String类的equals方法。但使用==比较两个String值是一个有效的表达式,它返回一个boolean值,并且可能在考试中使用。

现在是传递赋值操作到if结构中的难点部分。你认为以下代码的输出是什么?

图片

你可能会认为,由于boolean变量allow的值被设置为false,前面代码输出的值是false。重新审视代码,注意赋值操作allow = true将值true赋给boolean变量allow。此外,它的结果也是一个boolean值,这使得它可以作为参数传递给if结构。

尽管前面的代码没有语法错误,但存在一个逻辑错误——程序逻辑错误。将boolean变量与boolean字面量值比较的正确代码如下:

图片

考试技巧

在考试中注意使用赋值运算符(=)在if条件中比较boolean值。它不会比较boolean值;它会将其赋值。比较boolean值的正确运算符是相等运算符(==)。

5.1.5. 嵌套的if结构

嵌套的if结构是在另一个if结构内部定义的if结构。理论上,嵌套的ifif-else结构的层数没有限制。

每当遇到嵌套的ifif-else构造时,您需要小心确定if语句的else部分。如果前面的语句没有太多意义,请查看以下代码并确定其输出:

图片

根据代码的缩进方式,您可能会认为num-3.jpg处的else属于在num-1.jpg处定义的if。但实际上,它属于在num-2.jpg处定义的if。以下是正确缩进的代码:

图片

接下来,您需要了解如何执行以下操作:

  • 为除了默认分配给它的外部if之外的if定义一个else

  • 确定嵌套if构造中else属于哪个if

这两个任务都很简单。让我们从第一个开始。

为外部的 if 定义 else

关键点是使用花括号,如下所示:

图片

num-1.jpgnum-2.jpg处的花括号标记了在num-1.jpg处定义的if条件(score > 200)的开始和结束。因此,num-3.jpg处的else属于在num-1.jpg处定义的if

确定 else 属于哪个 if

如果代码使用花括号来标记ifelse构造的起始和结束,那么确定哪个else与哪个if匹配可能很简单,正如前文所述。当if构造不使用花括号时,不要被代码缩进所迷惑,缩进可能正确也可能不正确。

尝试匹配以下缩进不良的代码中的if与其相应的else

if (score > 200)
if (score < 400)
if (score > 300)
    System.out.println(1);
else
    System.out.println(2);
else
    System.out.println(3);

从内部开始工作,与最内层的if-else语句匹配,将每个else与其最近的未匹配if语句匹配。图 5.4 显示了如何匹配前面代码的if-else对,标记为num-1.jpgnum-2.jpg和![num-3.jpg]。

图 5.4. 如何匹配缩进不良的if-else

图片

图 5.5 显示了如果代码正确缩进(使用或不使用花括号),匹配if-else对变得多么容易。

图 5.5. 正确的代码缩进(带或不带花括号)使代码更易读。

图片

注意

作为良好的编程实践,正确缩进所有代码。同时使用花括号来提高可读性。

您还可以使用三元构造来评估一个表达式,并根据布尔表达式的结果将一个值分配给变量,这将在下一节中介绍。

5.1.6. 三元构造

您可以使用三元运算符?:来定义三元构造。三元构造可以与紧凑的if-else构造相媲美,用于根据布尔表达式分配一个值给变量。

正确用法

在下面的示例中,如果表达式(bill > 2000)评估为true,则变量discount被赋予值15,否则为10

图片

图 5.6 比较了前一个示例中使用的一个三元结构图片描述if-else结构。

图 5.6. 比较三元结构和if-else结构

图片描述

包围布尔表达式的括号是可选的,并且用于提高可读性。没有它们代码也能工作:

图片描述

变量discount可能或可能不在包含三元运算符的同一语句中声明:

图片描述

您也可以使用三元运算符将表达式赋值给变量discount。以下是修改后的代码:

图片描述

返回值的方法也可以用于在三元结构中初始化变量:

图片描述

不正确的使用

如果用于评估三元运算符的表达式不返回booleanBoolean值,则代码无法编译:

图片描述

三元运算符的所有三个部分都是必需的:

  • 布尔表达式

  • 如果布尔表达式评估为true时返回的值

  • 如果布尔表达式评估为false时返回的值

if结构不同,三元运算符不能省略其else部分。以下代码无法编译:

图片描述

在第三章中,您创建了返回值的方法。从这些方法返回的值不需要赋值,但这不适用于三元结构。三元运算符返回的值必须赋给变量,否则代码无法编译:

图片描述

因为三元运算符必须返回值,并将这些值赋给变量,所以它不能包含代码块。以下代码无法编译:

图片描述

不返回值的方法不能用于在三元结构中初始化变量:

图片描述

不正确的赋值

在考试中,要注意三元运算符返回的值与所赋变量的兼容性。以下代码无法编译,因为它试图将long值赋给int变量:

图片描述

此外,还要注意原始类型和包装类之间的转换。以下代码无法编译,因为Integer不能赋给Long,反之亦然:

图片描述

嵌套的三元结构

在以下示例中,三元运算符的if部分包含另一个三元运算符。在考试中要注意嵌套的三元结构:

图片描述

这是一个简单但有效的方法,可以将三元结构拆分并缩进到其组件中,以确定哪个值属于哪个运算符。让我们首先缩进一个简单三元结构,其中其布尔表达式、ifelse值分别放在不同的行上:

int discount = (bill > 1000)?
                    10
                    :9;

现在,让我们将这种缩进技术应用到本节开头使用的示例中:

int bill = 2000;
int qty = 10;

图片描述

在前面的代码中,? 结尾,即 boolean 表达式。 中的代码包含了嵌套的三元运算符的所有三个组成部分。如果 中的 boolean 表达式返回 true,则 中的代码将执行。 中的代码以 : 开头,并包含了 中三元运算符的 else 部分。

让我们将前面的缩进技术应用到另一个例子中。以下是缩进前的代码:

int bill = 2000;
int qty = 10;
int days = 10;
int discount = (bill > 1000)? (qty > 11)? 10 : days > 9? 20 : 30 : 5;
System.out.println(discount);

这是具有缩进的相同代码:

您还可以使用 switch 语句有条件地执行代码。尽管 if-else 构造和 switch 语句都用于选择性地执行语句,但它们的使用方式不同,您将在下一节使用 switch 语句时注意到这一点。

5.2. 选择语句

[3.4] 使用选择语句

在本节中,您将学习如何使用 switch 语句,并了解它与嵌套的 if-else 构造的比较。您将了解传递给 switch 标签的值的正确定义,以及在这些标签中正确使用 break 语句。

5.2.1. 创建和使用选择语句

您可以使用 switch 语句比较变量的值与多个值。对于这些值中的每一个,您可以定义一组要执行的语句。

以下示例使用 switch 语句将变量 marks 的值与使用 case 关键字定义的文本值 102030 进行比较:

一个 switch 语句可以在其 switch 块中定义多个 case 标签,但只能有一个 default 标签。当在 case 标签中找不到匹配的值时,将执行 default 标签。break 语句用于在匹配的 case 执行完成后退出 switch 语句。

5.2.2. 将选择语句与多个 if-else 构造进行比较

switch 语句可以通过用 switch 和多个 case 语句替换一系列(看起来相当复杂的)相关 if-else-if-else 语句来提高代码的可读性。

检查以下使用 if-else-if-else 语句检查 String 变量 day 的值并显示适当信息的代码:

现在检查使用 switch 语句实现前面代码的此实现:

String day = "SUN";
switch (day) {
    case "MON":
    case "TUE":

    case "WED":
    case "THU": System.out.println("Time to work");
                break;
    case "FRI": System.out.println("Nearing weekend");
                break;
    case "SAT":
    case "SUN": System.out.println("Weekend!");
                break;
    default: System.out.println("Invalid day?");
}

前面的两个代码片段执行的功能相同,即比较变量 day 的值并打印适当的值。但后者使用 switch 语句,代码更简单,更容易阅读和跟踪。

注意,前面的 switch 语句没有为所有 case 值定义代码。如果变量 day 的值匹配 TUE,会发生什么?当代码控制进入 switch 结构中匹配 TUE 的标签时,它将执行直到遇到 break 语句或到达 switch 语句的末尾的所有代码。

图 5.7 展示了本节示例代码中使用的多个 if-else-if-else 语句的执行过程。你可以将其比作一系列的问题和答案,直到找到匹配项或所有条件都被评估。

图 5.7。if-else-if-else 结构就像一系列的问题和答案。

if-else-if-else 结构相反,你可以将 switch 语句比作提出一个问题并评估其答案以确定要执行的代码。图 5.8 说明了 switch 语句及其 case 标签。

图 5.8。switch 语句就像提问并采取行动。

考试技巧

if-else-if-else 结构评估所有条件,直到找到匹配项。switch 结构将传递给它的参数与它的标签进行比较。

看看你能否在下一个练习中找到转折。提示:它定义了比较 String 值的代码(答案可在附录中找到)。

故事中的转折 5.2

按照前一个示例修改代码如下。这段代码的输出是什么?

String day = new String("SUN");
switch (day) {
    case "MON":
    case "TUE":
    case "WED":
    case "THU": System.out.println("Time to work");
                break;
    case "FRI": System.out.println("Nearing weekend");
                break;
    case "SAT":
    case "SUN": System.out.println("Weekend!");
                break;
    default: System.out.println("Invalid day?");
}
  1. 工作时间

  2. 接近周末

  3. 周末!

  4. 无效的日子?

5.2.3. 传递给 switch 语句的参数

你不能使用 switch 语句来比较所有类型的值,例如所有类型的对象和原始数据类型。switch 语句可以接受的参数类型有限制。

图 5.9 展示了可以传递给 switch 语句和 if 结构的参数类型。

图 5.9。可以传递给 switch 语句和 if 结构的参数类型

switch 语句接受 charbyteshortintString(从 Java 7 版本开始)类型的参数。它还接受 enumCharacterByteIntegerShort 类型的参数和表达式。由于枚举不是 OCA Java SE 8 程序员 I 考试目标,因此我不会进一步讨论它们。switch 语句不接受 longfloatdouble 类型的参数,或除了 String 之外的任何对象。

除了将变量传递给 switch 语句外,只要表达式返回允许的类型之一,你还可以将表达式传递给 switch 语句。以下代码是有效的:

以下代码无法编译,因为 history 的类型是 double,这是 switch 语句不接受的数据类型:

考试技巧

注意考试中试图将原始十进制类型(如floatdouble)传递给switch语句的问题。试图这样做的方法将无法编译。

对于非原始类型,即String和包装类型,switch参数不能为null,否则会抛出NullPointerException

在前面的代码中,如果变量value被赋予值10,代码将输出value is 10

考试技巧

对于非原始类型,即String和包装类型,switch参数不能为null,否则会抛出NullPointerException

5.2.4. 传递给 switch 语句标签情况的值

当涉及到可以传递给switch语句中case标签的值时,你会有一些限制,以下小节将解释。

情况值应该是编译时常量

case标签的值必须是编译时常量值;也就是说,该值应该在代码编译时已知:

注意,前面代码中在第处定义的b+c在编译时无法确定,是不允许的。但第处定义的10*7是一个有效的case标签值。

你可以在表达式中使用变量,如果它们被标记为final,因为一旦初始化,final变量的值就不能改变:

因为这里的变量bcfinal变量,所以在b+c的值可以在编译时确定。这使得它成为一个编译时常量值,可以用于case标签。

你可能会惊讶地发现,如果你在声明时没有为final变量分配值,它不被视为编译时常量:

此代码在第行定义了一个final变量c,但没有初始化它。final变量c在第行被初始化。因为final变量c在其声明中没有初始化,所以在处,表达式b+c不被视为编译时常量,因此不能用作情况标签。

情况值应该可以分配给传递给switch语句的参数

检查以下代码,其中传递给switch语句的参数类型是byte,而case标签值是float类型。这样的代码无法编译:

不允许将null用作情况标签

尝试将传递给switch语句的变量与null进行比较的代码无法编译,如下面的代码所示:

一个代码块可以为多个情况定义

switch语句中为多个case标签定义单个代码块是可以接受的,如下面的代码所示:

以下示例代码将在变量score的值匹配1005010中的任何一个时输出Average score

5.2.5. 在 switch 语句中使用 break 语句

在前面的例子中,请注意使用 break 语句退出 switch 结构,一旦找到匹配的情况。如果没有 break 语句,控制将 直接跳过 剩余的代码并执行所有 后续 的匹配情况对应的代码。

考虑图 5.10 figure 5.10 中显示的示例——一个带有 break 语句,另一个不带 break 语句。当变量 score 的值为 50 时,检查此图中的代码流程(使用箭头表示)。

图 5.10. 带与不带 break 语句的 switch 语句的代码流程差异

我们(假设的)热情的程序员 Harry 和 Selvan,他们也在准备这次考试,提交了一些他们的代码。你能在以下“故事转折”练习中选择他们的正确代码吗?(答案在附录中。)

故事转折 5.3

在我们两位假设的程序员 Harry 和 Selvan 的以下代码提交中,哪个检查了长变量 dayCount 的值并打印出任何与日计数匹配的月份名称?

  1. Harry 的提交:

    long dayCount = 31;
    if (dayCount == 28 || dayCount == 29)
        System.out.println("Feb");
    else if (dayCount == 30)
        System.out.println("Apr");
    else if (dayCount == 31)
        System.out.println("Jan");
    
  2. Selvan 的提交:

    long dayCount = 31;
    switch (dayCount) {
        case 28:
        case 29: System.out.println("Feb"); break;
        case 30: System.out.println("Apr"); break;
        case 31: System.out.println("Jan"); break;
    }
    

在下一节中,我将介绍称为循环语句的迭代语句。就像你每天都想重复“吃冰淇淋”的动作一样,循环用于多次执行相同的代码行。你可以使用 for 循环、增强型 for (for-each) 循环或 do-whilewhile 循环来重复代码块。让我们从 for 循环开始。

5.3. for 循环

[5.2] 创建和使用 for 循环,包括增强型 for 循环

在本节中,我将介绍常规或传统的 for 循环。增强型 for 循环将在下一节介绍。

for 循环通常用于执行一组语句固定次数。它具有以下形式:

for (initialization; condition; update) {
     statements;
}

这里有一个简单的例子:

前面代码的输出如下:

25
50
75
100
125

在前面的例子中,位于 的代码将执行五次。它将从变量 ctr 的初始值 1 开始执行,并且只要变量 ctr 的值小于或等于 5,就会继续执行。变量 ctr 的值在执行位于 的代码后会递增 1 (ctr++)。

位于 的代码将对 ctr12345 执行。因为 6 <= 5 评估为 false,所以 for 循环在执行位于 的代码后完成其执行,不再执行任何进一步的代码。

在前面的例子中,请注意 for 循环定义了三种类型的语句,这些语句由分号 (;) 分隔,如下所示:

  • 初始化语句

  • 终止条件

  • 更新子句(可执行语句)

这个循环在图 5.11 中用流程图表示。循环体中定义的语句会一直执行,直到终止条件为false

图 5.11. for循环中的控制流程

图片

关于for循环,需要注意的一个重要点是更新子句在for循环体内部定义的所有语句之后执行。换句话说,你可以将更新子句视为for循环中的最后一个语句。初始化部分只执行一次,可以定义多个初始化语句。同样,更新子句也可以定义多个语句。但for循环只能有一个终止条件。

在图 5.12 中,我提供了一个代码片段和流程图,以展示语句执行的相应流程,从而解释前面的概念。

图 5.12. 使用代码示例的for循环控制流程

图片

让我们详细探讨for循环的初始化块、终止条件和更新子句。

5.3.1. 初始化块

初始化块只执行一次。for循环可以在其初始化块中声明和初始化多个变量,但声明的变量应该是同一类型的。以下代码是有效的:

图片

但你无法在初始化块中声明不同类型的变量。以下代码将无法编译:

图片

尝试在for的初始化块外部使用在初始化块中定义的变量是一个常见的编程错误。请注意,初始化块中声明的变量的作用域仅限于for块。以下是一个例子:

图片

5.3.2. 终止条件

终止条件在执行循环体中定义的语句之前对每次迭代进行评估。当终止条件评估为false时,for循环终止:

图片

终止条件——在这个例子中是ctr <= 5——在执行图片之前被检查。如果条件评估为false,控制权转移到图片for循环可以定义一个精确的终止条件——不多也不少。

5.3.3. 更新子句

通常,你会使用这个块来操作用于指定终止条件的变量的值。在之前的例子中,我定义了以下代码:

++ctr;

定义在此块中的代码将在for循环体中定义的所有代码之后执行。在以下代码执行后,前面的代码将变量ctr的值增加1

System.out.println(ctr);

终止条件接下来被评估。这个执行会一直持续到终止条件评估为false

你可以在更新子句中定义多个语句,包括对其他方法的调用。唯一的限制是这些语句将按照它们出现的顺序执行,在for块中定义的所有语句之后执行。检查以下代码,它在更新块中调用了一个方法:

图片

此代码的输出如下:

a
Happy
b
Happy

5.3.4. for 语句的可选部分

for语句的三个部分——即初始化块、终止条件和更新子句——都是可选的。但你必须通过只包含分号来指定不包括某个部分。在以下示例中,初始化块不包含任何代码:

图片

移除标记初始化块结束的分号会阻止代码编译:

图片

在以下示例中,终止条件缺失,导致可能无限循环(可能因为可以使用break语句或异常来停止):

图片

再次,如果你移除标记终止条件结束的分号,代码将无法编译:

图片

以下代码在其更新子句中不包含代码,但编译成功:

图片

但移除标记更新子句开始的分号会阻止代码编译:

图片

有趣的是,以下代码是有效的:

for(;;)
    System.out.println(1);
考试技巧

for语句的三个部分——初始化块、终止条件和更新子句——都是可选的。缺少终止条件意味着无限循环。

5.3.5. 嵌套 for 循环

如果一个循环包含另一个循环,它们被称为嵌套循环。包含另一个循环的循环被称为外循环,而被包含的循环被称为内循环。理论上,嵌套for循环的层级没有限制。

让我们从单级嵌套循环开始。例如,你可以将时钟的时针比作外循环,而分针比作内循环。每个小时可以与外循环的一次迭代相比较,而每分钟可以与内循环的一次迭代相比较。因为一小时有 60 分钟,所以内循环应该在外循环的每次迭代中迭代 60 次。时钟与嵌套循环之间的比较如图 5.13 所示。

图 5.13. 时钟指针与嵌套循环的比较

图片

你可以使用以下嵌套for循环来打印从16小时的每一分钟(160):

图片

嵌套循环常用于初始化或迭代多维数组。以下代码使用嵌套for循环初始化多维数组:

图片

定义了一个二维数组 multiArr 分配了这个数组,创建了两个行和三个列,并将所有数组成员赋值为默认的 int0。图 5.14 展示了使用前面的代码初始化后的 multiArr 数组。

图 5.14. 初始化后的数组 multiArr

定义了一个外层 for 循环。因为 multi-Arr.length 的值为 2(如 中第一个下标的值),外层 for 循环执行两次,变量 i 的值为 01。内层 for 循环在 中定义。因为 multiArr 数组的每一行的长度为 3(如 中第二个下标的值),内层循环在外层 for 循环的每次迭代中执行三次,变量 j 的值为 012

在下一节中,我将讨论 for 循环的另一种风味:增强型 for 循环或 for-each 循环。

5.4. 增强型 for 循环

[5.2] 创建和使用包括增强型 for 循环在内的循环

增强型 for 循环也被称为 for-each 循环,它相对于常规 for 循环提供了一些优势。它也有一些限制。

5.4.1. 使用增强型 for 循环进行迭代

首先,当涉及到遍历集合或数组时,常规 for 循环使用起来比较繁琐。即使你想遍历整个集合或列表,也需要创建一个循环变量并指定集合或数组的起始和结束位置。增强型 for 循环使得之前提到的常规任务变得非常简单,以下示例展示了对于 ArrayList myList 的使用:

ArrayList<String> myList= new ArrayList<String>();
myList.add("Java");
myList.add("Loop");

以下代码使用常规 for 循环遍历此列表:

for(Iterator<String> i = myList.iterator(); i.hasNext();)
    System.out.println(i.next());

此代码使用增强型 for 循环遍历列表 myList

for (String val : myList)
    System.out.println(val);

你可以将 for-each 循环中的冒号(:)读作“在”。

for-each 循环的实现非常简单:没有代码杂乱,代码易于编写和理解。在上面的例子中,for-each 循环被读作“对于集合 myList 中的每个元素 val,打印 val 的值。”

你也可以使用增强型 for 循环轻松地遍历 嵌套集合。在这个例子中,假设定义了一个包含 examslevelsgradesArrayList,如下所示:

ArrayList<String> exams= new ArrayList<String>();
exams.add("Java");
exams.add("Oracle");
ArrayList<String> levels= new ArrayList<String>();
levels.add("Basic");
levels.add("Advanced");
ArrayList<String> grades= new ArrayList<String>();
grades.add("Pass");
grades.add("Fail");

以下代码创建了一个嵌套的 ArrayListnestedArrayList,其中每个元素本身都是一个 String 对象的 ArrayList

nestedArrayList 可以与多维数组进行比较,如图 5.15 所示。

图 5.15. nestedArrayList 的示意图

可以使用嵌套增强型 for 循环遍历嵌套的 ArrayList nestedArrayList。以下是相关代码:

for (ArrayList<String> nestedListElement : nestedArrayList)
    for (String element : nestedListElement)
        System.out.println(element);

这段代码的输出如下:

Java
Oracle
Basic
Advanced
Pass
Fail

增强型for循环再次轻松地用于遍历*嵌套**非嵌套*数组。例如,你可以使用以下代码遍历一个元素数组并计算其总和:

int total = 0;
int primeNums[] = {2, 3, 7, 11};
for (int num : primeNums)
    total += num;

当你尝试在增强型for循环中修改循环变量的值时会发生什么?结果取决于你是在遍历原始值集合还是对象集合。如果你是在遍历原始值数组,由于在增强型for循环中原始值是按值传递给循环变量的,因此操作循环变量永远不会改变正在遍历的数组的值。

当你遍历一个对象集合时,集合的值是通过引用传递给循环变量的。因此,如果你通过在它上执行方法来操作循环变量的值,修改后的值将反映在正在遍历的对象集合中:

前面代码的输出是

Java
Loop
JavaOracle
LoopOracle

让我们修改前面的代码。不是在循环变量val上调用append方法,而是给它分配另一个StringBuilder对象。在这种情况下,正在遍历的数组的原始元素将不会受到影响,将保持不变:

前面代码的输出是

Java
Loop
Java
Loop
考试技巧

注意使用增强型for循环及其循环变量来更改它遍历的集合中元素值的代码。这种行为经常成为考试出题者的思考素材。

5.4.2. 增强型for循环的限制

虽然for-each循环是遍历集合和数组的不错选择,但它不能用在某些地方。

不能用于初始化数组并修改其元素

你能否在以下代码中使用增强型for循环代替常规for循环?

简单的答案是:不可以。虽然你可以在增强型for循环外部定义一个计数器,并使用它来初始化和修改数组元素,但这种做法违背了for-each循环的目的。在这种情况下,传统的for循环更容易使用。

不能用于删除或移除集合的元素

因为for循环隐藏了用于遍历集合元素的*iterator*,所以你不能用它来移除或删除现有的集合值,因为你不能调用remove方法。

如果你将null值赋给循环变量,它不会从集合中移除元素:

前面代码的输出是

One
Two
One
Two
不能在同一个循环中遍历多个集合或数组

虽然使用for循环遍历嵌套集合或数组是完全可行的,但你不能在同一个for-each 循环中遍历多个集合或数组,因为for-each 循环只能创建一个循环变量。与常规for循环不同,你无法在for-each 循环中定义多个循环变量。

考试技巧

使用for-each 循环遍历数组和集合。不要用它来初始化、修改或过滤它们。

5.4.3. 嵌套增强型 for 循环

首先,处理嵌套集合与处理嵌套循环并不相同。嵌套循环也可以与无关的集合一起工作。

如 5.3.4 节中所述,定义在另一个循环内的循环称为嵌套循环。定义自身内部另一个循环的循环称为外层循环,而定义在另一个循环内的循环称为内层循环。理论上,任何循环的嵌套级别都没有限制,包括增强型for循环。

在本节中,我们将使用三个嵌套的增强型for循环。你可以将三级嵌套循环比作一个有小时、分钟和秒针的时钟。时钟的秒针每分钟完成一圈。同样,分针每小时完成一圈。这种比较在图 5.16 中显示。

图 5.16. 三手钟与嵌套for循环级别的比较

图片

以下是一个嵌套增强型for循环的编码示例,我在前一个章节中讨论过:

图片

嵌套循环中的内层循环在其外层循环的每次迭代中执行。前面的例子定义了三个增强型for循环:最外层循环在图片,内层嵌套循环在图片,最内层循环在图片。在图片中的完整最内层循环在其直接外层循环(在图片中定义)的每次迭代中执行。同样,在图片中定义的完整内层循环在其直接外层循环(在图片中定义)的每次迭代中执行。图 5.17 显示了所有这些循环迭代的循环值。

图 5.17. 嵌套for循环及其迭代的循环值

图片

上述代码的输出如下:

Java:Basic:Pass
Java:Basic:Fail
Java:Advanced:Pass
Java:Advanced:Fail
Oracle:Basic:Pass
Oracle:Basic:Fail
Oracle:Advanced:Pass
Oracle:Advanced:Fail
考试技巧

嵌套循环在其直接外层循环的每次迭代中执行所有迭代。

除了for循环之外,考试中的其他循环语句是whiledo-while,这些将在下一节中讨论。

5.5. 循环和 do-while 循环

[5.1] 创建和使用 while 循环

[5.3] 创建和使用 do-while 循环

你将在本节中学习 whiledo-while 循环。这两个循环只要它们的条件评估为 true 就会执行一组语句。这两个循环的工作方式相同,除了一个区别:while 循环在评估其循环体之前检查其条件,而 do-while 循环在执行其循环体中定义的语句之后检查其条件。

这种行为上的差异是否会影响它们的执行?是的,它会影响,在本节中,你将看到这一点。

5.5.1. while 循环

while 循环用于反复执行一组语句,只要其条件评估为 true。这个循环在开始执行语句之前检查条件。

例如,在著名的快餐连锁店 Superfast Burgers,员工可能被指示只要面包有供应就准备汉堡。在这个例子中,面包的可用性是 while 条件,而准备汉堡是 while 循环的循环体。你可以用以下代码表示这一点:

boolean bunsAvailable = true;
while (bunsAvailable) {
    /* ... prepare burger ...*/
    if (noMoreBuns)
        bunsAvailable = false;
}

上述例子仅用于演示目的,因为循环体并没有完全定义。在 while 循环中用于检查是否再次执行循环体的条件应该在某个时刻评估为 false;否则,循环将无限执行。这个循环变量的值可能被 while 循环或另一个方法(如果它是实例或 static 变量)更改。

while 循环接受 booleanBoolean 类型的参数。在前面的代码中,循环体检查是否还有面包可用。如果没有面包可用,它将变量 bunsAvailable 的值设置为 false。由于 bunsAvailable 评估为 false,循环体在下一个迭代中不会执行。

前面的 while 循环的执行过程在 图 5.18 中以简单的流程图的形式展示,以帮助你更好地理解这个概念。

图 5.18. 描述 while 循环中代码流程的流程图

现在,让我们来考察另一个使用 while 循环的简单例子:

int num = 9;
boolean divisibleBy7 = false;
while (!divisibleBy7) {
    System.out.println(num);
    if (num % 7 == 0) divisibleBy7 = true;
    --num;
}

上述代码的输出如下:

9
8
7

如果你将代码更改为如下(粗体表示更改),会发生什么(更改如下)?

int num = 9;
boolean divisibleBy7 = true;
while (divisibleBy7 == false) {
    System.out.println(num);
    if (num % 7 == 0) divisibleBy7 = true;
    --num;
}

由于条件 divisibleBy7==false 评估为 false,代码不会进入循环。

5.5.2. do-while 循环

do-while 循环用于反复执行一组语句,直到它所使用的条件评估为 false。这个循环在完成其循环体内的所有语句的执行后检查条件。

你可以将这种结构比作一个在启动时显示菜单的软件应用程序。每个菜单选项将执行一系列步骤并重新显示菜单。最后一个菜单选项是“退出”,它将退出应用程序并且不会重新显示菜单:

boolean exitSelected = false;
do {
    String selectedOption = displayMenuToUser();
    if (selectedOption.equals("exit"))
        exitSelected = true;
    else
        executeCommand(selectedOption);
} while (exitSelected == false);

上述代码用一个简单的流程图表示在 图 5.19,这将帮助你更好地理解代码。

图 5.19. 流程图显示do-while循环中的代码流程

上述示例仅用于演示目的,因为do-while循环中使用的方法尚未定义。正如前一个关于while循环的章节所讨论的,用于在do-while循环中检查是否再次执行循环体的条件应该在某个时刻评估为false,否则循环将无限执行。此循环变量的值可以通过while循环或另一种方法(如果它是实例或static变量)来更改。

注意

不要忘记在指定条件后使用分号(;)来结束do-while循环。即使一些经验丰富的程序员也会忽略这一步!

do-while循环接受类型为booleanBoolean的参数。

让我们修改第 5.5.1 节中使用的示例,使用do-while循环而不是while循环,如下所示:

int num = 9;
boolean divisibleBy7 = false;
do {
    System.out.println(num);
    if (num % 7 == 0) divisibleBy7 = true;
    num--;
} while (divisibleBy7 == false);

此代码的输出如下:

9
8
7

如果你按如下方式更改代码会发生什么(粗体表示更改)?

int num = 9;
boolean divisibleBy7 = true;
do {
    System.out.println(num);
    if (num % 7 == 0) divisibleBy7 = true;
    num--;
} while (divisibleBy7 == false);

上述代码的输出如下:

9

do-while循环至少执行一次,即使do-while循环中指定的条件评估为false,因为条件是在执行循环体之后评估的。

5.5.3. whiledo-while块、表达式和嵌套规则

你可以使用花括号{}whiledo-while循环一起使用,为每次迭代定义要执行的多个代码行。如果不使用花括号,则只有第一行代码被视为whiledo-while循环的一部分,如第 5.1.3 节中指定的if-else结构。

同样,定义传递给whiledo-while循环的适当表达式的规则与第 5.1.4 节中的if-else结构相同。此外,定义嵌套whiledo-while循环的规则与第 5.1.5 节中的if-else结构相同。

5.6. 比较循环结构

[5.4] 比较循环结构

在本节中,我将讨论以下循环结构之间的差异和相似之处:do-whilewhilefor和增强型for

5.6.1. 比较do-whilewhile循环

do-whilewhile循环都会执行一组语句,直到它们的终止条件评估为false。这两个语句之间的唯一区别是do-while循环即使在条件评估为false的情况下也会执行代码,do-while循环在执行语句之后评估终止条件,而while循环在执行其语句之前评估终止条件。

这些语句的形式如图 5.20 所示。

图 5.20. 比较do-whilewhile循环

你认为以下代码的输出是什么?

上述代码的输出如下:

12

你认为以下代码的输出是什么?

上述代码的输出如下:

11

5.6.2. 比较 for 循环和增强 for 循环

正规的 for 循环虽然使用起来有些繁琐,但比增强的 for 循环(如第 5.4.1 节中提到的)功能更强大。

  • 增强的 for 循环不能用于初始化数组并修改其元素-。

  • 增强的 for 循环不能用于删除集合的元素。

  • 增强的 for 循环不能用于在同一个循环中迭代多个集合或数组。

5.6.3. 比较 for 循环和 while 循环

当你知道迭代次数时,你应该 尝试 使用 for 循环——例如,当你正在遍历一个集合或数组,或者当你需要执行固定次数的循环,比如 ping 服务器五次时。

当你事先不知道迭代次数,并且迭代次数取决于一个条件为 true 时,你应该 尝试 使用 do-whilewhile 循环——例如,当你接受护照续签申请直到没有更多申请人时。在这种情况下,你将不知道在给定的一天有多少申请人提交了他们的申请。

5.7. 循环语句:break 和 continue

[5.5] 使用 break 和 continue

想象一下,你已经定义了一个循环来遍历经理列表,并且你正在寻找至少一个名字以字母 D 开头的经理。你希望在找到第一个匹配项后退出循环,但如何做到呢?你可以在循环中使用 break 语句来实现这一点。

现在想象一下,你想要遍历笔记本电脑上的所有文件夹,并扫描大于 10 MB 的文件是否有病毒。如果所有这些文件都被发现是安全的,你希望将它们上传到服务器。但如果你想要跳过小于 10 MB 文件大小的 病毒检查文件上传 步骤,同时仍然处理笔记本电脑上的剩余文件,你可以做到!你可以在循环中使用 continue 语句。

在本节中,我将讨论 breakcontinue 语句,你可以使用这些语句完全退出循环或跳过循环迭代中的剩余语句。在本节的最后,我将讨论带标签的语句。

5.7.1. break 语句

break 语句用于 退出跳出 forfor-each、dodo-while 循环,以及 switch 构造。或者,可以使用 continue 语句来跳过当前迭代中的剩余步骤,并从下一个循环迭代开始。

这些语句之间的区别可以通过以下示例最好地展示。你可以使用以下代码来浏览并打印 String 数组中的所有值:

String[] programmers = {"Paul", "Shreya", "Selvan", "Harry"};
for (String name : programmers) {
    System.out.println(name);
}

上述代码的输出如下:

Paul
Shreya
Selvan
Harry

让我们修改前面的代码,以便在数组值等于 Shreya 时退出循环。以下是所需的代码:

图片

上述代码的输出如下:

Paul

一旦循环遇到 break,它就会退出循环。因此,只打印这个数组的第一个值——即 Paul。如 switch 构造部分的说明中所述,可以在每个 case 后定义 break 语句,以便一旦找到匹配的 case,控制就会退出 switch 构造。

前面的代码片段在 图 5.21 中展示,该图显示了执行 break 语句时的控制转移。

图 5.21. 在循环中执行 break 语句时的控制流

图片

当你在嵌套循环中使用 break 语句时,它会退出内部循环。下一个“故事中的转折”练习将查看一个小代码片段,以了解在嵌套 for 循环中使用 break 语句时控制如何转移(答案见附录)。

故事中的转折 5.4

将上一个示例中使用的代码修改如下。这段代码的输出是什么?

String[] programmers = {"Outer", "Inner"};
for (String outer : programmers) {
    for (String inner : programmers) {
        if (inner.equals("Inner"))
            break;
        System.out.print(inner + ":");
    }
}
  1. 外部:外部:

  2. 外部:内部:外部:内部:

  3. 外部:

  4. 外部:内部:

  5. 内部:内部:

5.7.2. continue 语句

continue 语句用于跳过当前迭代的剩余步骤,并从下一个循环迭代开始。让我们将上一个例子中的 break 语句替换为 continue 并查看其输出:

图片

上述代码的输出如下:

Paul
Selvan
Harry

一旦循环遇到 continue,它就会退出当前迭代。在这个例子中,它跳过了数组值 Shreya 的打印步骤。与 break 语句不同,continue 不会退出循环——它重新开始下一个循环迭代,打印剩余的数组值(即 SelvanHarry)。

当你在嵌套循环中使用 continue 语句时,它会退出内部循环的当前迭代。

图 5.22 比较了在循环中使用 breakcontinue 语句时控制如何转移出循环和到下一个迭代。

图 5.22. 在循环中使用 breakcontinue 语句时控制流的比较

图片

5.7.3. 带标签的语句

在 Java 中,你可以给以下类型的语句添加标签:

  • 使用 {} 定义的代码块

  • 所有循环语句(for、增强型 forwhiledo-while

  • 条件结构(ifswitch 语句)

  • 表达式

  • 作业

  • return 语句

  • try

  • throws 语句

这里给出了一个带标签的循环示例:

String[] programmers = {"Outer", "Inner"};
outer:
for (int i = 0; i < programmers.length; i++) {
}

你不能给声明添加标签。以下带标签的声明无法编译:

图片

有趣的是,前面的声明可以定义在块语句中,如下所示:

图片

带标签的 break 语句

你可以使用带标签的 break 语句退出外部循环。以下是一个示例:

图片

上述代码的输出是

Outer:

当此代码执行 break outer; 时,控制权转移到标记此代码块结束的文本行。它不会将控制权转移到标签 outer

带标签的 continue 语句

你可以使用带标签的 continue 语句来跳过外部循环的迭代。以下是一个示例:

上述代码的输出是

Paul
Paul
Paul
Paul
注意

请谨慎使用标签,并且只有当它们 真正 增加代码可读性时才使用。

5.8. 摘要

我们从本章的开始讲到了选择语句 ifswitch 以及三元构造。我们涵盖了 if 构造的不同形式。然后我们探讨了 switch 构造,它接受包括 bytecharshortintString 在内的有限类型的参数。谦逊的 if-else 构造可以定义几乎任何简单或复杂的条件集。

你也看到了如何使用所有类型的循环来执行你的代码:forfor-each、dodo-whilefordodo-while 循环自从 Java 语言首次引入以来一直存在,而增强型 for 循环(即 for-each 循环)是在 Java 5.0 版本中添加到语言中的。我建议你使用 for-each 循环来遍历数组和集合。

在本章末尾,我们探讨了 breakcontinue 语句。你使用 break 语句来 退出跳出 forfor-each、dodo-whileswitch 构造。你使用 continue 语句来跳过当前迭代的剩余步骤,并从下一个循环迭代开始。

5.9. 复习笔记

ifif-else 构造:

  • if 语句允许你根据条件的计算结果执行代码中的一组语句,该条件应该评估为 booleanBoolean 值。

  • if 语句的多种形式是 ifif-elseif-else-if-else

  • if 构造不使用关键字 then 来定义当 if 条件评估为 true 时要执行的代码。if 构造的 then 部分跟在 if 条件后面。

  • if 构造可能或可能不定义其 else 部分。

  • if 构造的 else 部分不能在没有定义其 then 部分的情况下存在。

  • 容易与其他编程语言中使用的常见 if-else 语法混淆。Java 中不使用 if-elsifif-elseif 语句来定义 if-else-if-else 构造。正确的关键字是 ifelse

  • 你可以为相应的 truefalse 条件执行单个语句或语句块。一对花括号 {} 标记一个语句块。

  • 如果 if 构造没有使用 {} 来定义其 thenelse 部分的代码块,则只有第一行代码是 if 构造的一部分。

  • boolean变量的赋值也作为参数传递给if结构是有效的,因为结果值是boolean,这是if结构所接受的。

  • 从理论上讲,嵌套的ifif-else结构在层级上没有限制。当使用嵌套的if-else结构时,请注意将else部分与正确的if部分匹配。

三元结构:

  • 您可以使用三元运算符?:来定义一个紧凑的if-else结构,根据布尔表达式为变量赋值。

  • 为了提高可读性,布尔表达式的括号是可选的。没有它们代码仍然可以工作。

  • 您可以使用三元运算符将字面值或表达式赋给变量。

  • 可以使用返回值的method来初始化三元结构中的变量。

  • 如果用于评估三元运算符的表达式没有返回booleanBoolean值,代码将无法编译。

  • 三元运算符的所有三个部分都是必需的。

  • 三元运算符返回的值必须被分配给变量,否则代码将无法编译。

  • 因为三元运算符必须返回值,这些值将被分配给变量,所以它不能包含代码块。

  • 不返回值的method不能用于在三元结构中初始化变量。

  • 三元结构返回的值必须与被赋值的变量类型兼容。

  • 三元运算符可以嵌套到任何层级。

switch语句:

  • switch语句用于将变量的值与多个预定义的值进行比较。

  • switch语句接受类型为charbyteshortintString的参数。它还接受包装类参数:CharacterByteShortIntegerEnum

  • 可以将switch语句与多个相关的if-else-if-else结构进行比较。

  • 只要表达式的类型是可接受的数据类型之一,您就可以将表达式作为参数传递给switch语句。

  • case值应该是编译时常量,可以分配给传递给switch语句的参数。

  • case值不能是字面值null

  • case值可以定义使用字面值的表达式;也就是说,它们可以在编译时评估,就像7+2一样。

  • 可以定义一个代码块,用于在switch语句中执行多个case值。

  • 一旦找到匹配的case并执行了所需的代码语句,break语句用于退出switch结构。

  • 如果没有break语句,控制将贯穿switch语句中剩余的所有case值,直到找到第一个break语句,按顺序评估case语句中的代码。

for循环:

  • 传统for循环通常用于执行一组语句固定次数。

  • for循环通过分号(;)定义了三种类型的语句:初始化语句、终止条件和更新子句。

  • 三个for语句中的任何一个——初始化语句、终止条件和更新子句的定义都是可选的。例如,for (;;);for (;;){}都是定义for循环的有效代码。同样,定义这些语句中的任何一个也是有效的代码。

  • 初始化块只执行一次。for循环可以在其初始化块中声明和初始化多个变量,但声明的变量应该是同一类型的。

  • 终止条件在执行循环体内部的语句之前,对每次迭代只评估一次。

  • for循环在终止条件评估为false时终止。

  • 更新块通常用于增加或减少在初始化块中定义的变量的值。它还可以执行多个其他语句,包括方法调用。

  • 嵌套的for循环在层级上没有限制。

  • 嵌套的for循环经常用于处理多维数组。

增强的for循环:

  • 增强的for循环也被称为for-each循环。

  • 增强的for循环在常规for循环之上提供了一些好处,但它不如常规for循环灵活。

  • 增强的for循环提供了简单的语法来遍历值集合——一个数组、ArrayList或 Java 集合框架中的其他类,这些类存储了值集合。

  • 增强的for循环不能用于初始化数组并修改其元素。

  • 增强的for循环不能用于删除集合的元素。

  • 增强的for循环不能在同一个循环中迭代多个集合或数组。

  • 嵌套的增强for循环在层级上没有限制。

whiledo-while循环:

  • while循环用于在条件评估为false之前,持续执行一组语句。这个循环在开始执行语句之前会检查条件。

  • do-while循环用于在条件评估为false之后,持续执行一组语句。这个循环在完成其循环体内的所有语句的执行后会检查条件。

  • 嵌套的do-whilewhile循环的层级没有限制。

  • do-while循环和while循环都可以定义一行代码或一个代码块来执行。后者是通过使用花括号{}来定义的。

比较循环结构:

  • do-whilewhile循环都会执行一组语句,直到终止条件评估为false。这两个语句之间的唯一区别是do-while循环至少会执行一次代码,即使条件评估为false

  • 正规的for循环虽然使用起来有些繁琐,但比增强的for循环更强大。

  • 增强的for循环不能用于初始化数组并修改其元素。增强的for循环不能用于删除或移除集合的元素。

  • 增强的for循环不能在同一个循环中迭代多个集合或数组。

  • 当你知道迭代次数时,你应该尝试使用for循环——例如,遍历一个集合或数组,或者执行固定次数的循环,比如 ping 服务器五次。

  • 当你事先不知道迭代次数,且迭代次数取决于某个条件为真时,你应该尝试使用do-whilewhile循环——例如,接受护照更新申请直到所有申请人都被处理。

循环语句(breakcontinue):

  • break语句用于退出——或跳出——forfor-each、dodo-while循环和switch构造。

  • continue语句用于跳过当前迭代的剩余步骤,并从下一个循环迭代开始。continue语句与forfor-each、dodo-while循环以及switch构造一起使用。

  • 当你在嵌套循环中使用break语句时,它将退出相应的循环。

  • 当你在嵌套循环中使用continue语句时,它将退出当前循环的迭代。

带标签的语句:

  • 你可以在使用大括号{}定义的代码块中添加标签,所有循环语句(for、增强的forwhiledo-while)、条件构造(ifswitch语句)、表达式和赋值、return语句、try块和throws语句。

  • 你不能在变量的声明中添加标签。

  • 你可以使用带标签的break语句退出外层循环。

  • 你可以使用带标签的continue语句来跳过外层循环的迭代。

5.10. 样本考试问题

Q5-1.

以下代码的输出是什么?

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

        do
            while (i < 15)
                i = i + 20;
        while (i < 2);
        System.out.println(i);
    }
}
  1. 10
  2. 30
  3. 31
  4. 32

Q5-2.

以下代码的输出是什么?

class Loop2 {
    public static void main(String[] args) {
        int i = 10;
        do
            while (i++ < 15)
                i = i + 20;
        while (i < 2);
        System.out.println(i);
    }
}
  1. 10
  2. 30
  3. 31
  4. 32

Q5-3.

以下哪个陈述是正确的?

  1. 增强的for循环不能在常规for循环中使用。
  2. 增强的for循环不能在while循环中使用。
  3. 增强的for循环可以在do-while循环中使用。
  4. 增强的for循环不能在switch构造中使用。
  5. 所有的上述陈述都是错误的。

Q5-4.

以下代码的输出是什么?

int a =  10;
if (a++ > 10) {
    System.out.println("true");
}
{
    System.out.println("false");
}
System.out.println("ABC");
  1. true
    false
    ABC
    
  2. false
    ABC
    
  3. true
    ABC
    
  4. 编译错误

Q5-5.

给定以下代码,以下哪条可选代码可以单独替换//INSERT CODE HERE行,以便代码成功编译?

class EJavaGuru {
    public static void main(String args[]) {
        int num = 10;
        final int num2 = 20;
        switch (num) {
            // INSERT CODE HERE
            break;
            default: System.out.println("default");
        }
    }
}
  1. case 10*3: System.out.println(2);
  2. case num: System.out.println(3);
  3. case 10/3: System.out.println(4);
  4. case num2: System.out.println(5);

Q5-6.

以下代码的输出是什么?

class EJavaGuru {
    public static void main(String args[]) {
        int num = 20;
        final int num2;
        num2 = 20;
        switch (num) {
            default: System.out.println("default");
            case num2: System.out.println(4);
            break;
        }
    }
}
  1. default
  2. default
    4
    
  3. 4
  4. 编译错误

Q5-7.

以下代码的输出是什么?

class EJavaGuru {
    public static void main(String args[]) {
        int num = 120;
        switch (num) {
            default: System.out.println("default");
            case 0: System.out.println("case1");
            case 10*2-20: System.out.println("case2");
            break;
        }
    }
}
  1. default
    case1
    case2
    
  2. case1
    case2
    
  3. case2
  4. 编译错误
  5. 运行时异常

Q5-8.

以下代码的输出是什么?

class EJavaGuru3 {
    public static void main(String args[]) {
        byte foo = 120;
        switch (foo) {
            default: System.out.println("ejavaguru"); break;
            case 2: System.out.println("e"); break;
            case 120: System.out.println("ejava");
            case 121: System.out.println("enum");
            case 127: System.out.println("guru"); break;
        }
    }
}
  1. ejava
    enum
    guru
    
  2. ejava
  3. ejavaguru
    e
    
  4. ejava
    enum
    guru
    ejavaguru
    

Q5-9.

以下代码的输出是什么?

class EJavaGuru4 {
    public static void main(String args[]) {
        boolean myVal = false;
        if (myVal=true)
        for (int i = 0; i < 2; i++) System.out.println(i);
        else System.out.println("else");
    }
}
  1. else
    
  2. 0
    1
    2
    
  3. 0
    1
    
  4. 编译错误

Q5-10.

以下代码的输出是什么?

class EJavaGuru5 {
    public static void main(String args[]) {
        int i = 0;
        for (; i < 2; i=i+5) {
            if (i < 5) continue;
            System.out.println(i);
        }
        System.out.println(i);
    }
}
  1. 编译错误
  2. 0
    5
    
  3. 0
    5
    10
    
  4. 10
  5. 0
    1
    5
    
  6. 5

5.11. 样本考试问题答案

Q5-1.

以下代码的输出是什么?

class Loop2 {
    public static void main(String[] args) {
        int i = 10;
        do
            while (i < 15)
                i = i + 20;
        while (i < 2);
        System.out.println(i);
    }
}
  1. 10
  2. 30
  3. 31
  4. 32

答案:b

解释:do-while循环中指定的条件评估为false(因为10<2评估为false)。但是,由于do-while循环至少执行一次——其条件在循环结束时检查,因此控制进入do-while循环。while循环在第一次迭代时评估为true,并将20加到i上,使其变为30while循环没有第二次执行。因此,前一段代码执行结束时变量i的值是30

Q5-2.

以下代码的输出是什么?

class Loop2 {
    public static void main(String[] args) {
        int i = 10;
        do
            while (i++ < 15)
                i = i + 20;
        while (i < 2);
        System.out.println(i);
    }
}
  1. 10
  2. 30
  3. 31
  4. 32

答案:d

解释:如果你尝试回答问题 5-1,你可能会为这个问题选择相同的答案。我故意使用了相同的问题文本和变量名(略有不同),因为你在 OCA Java SE 8 程序员 I 考试中可能会遇到类似的模式。这个问题包括一个差异:与问题 5-1 不同,它使用后缀一元运算符在while条件中。

do-while循环中指定的条件评估为false(因为10<2评估为false)。但是,由于do-while循环至少执行一次——其条件在循环结束时检查,因此控制进入do-while循环。这个问题打印出32,而不是30,因为while循环中指定的条件(具有增量运算符)执行了两次。

在这个问题中,while循环的条件执行了两次。对于第一次评估,i++ < 15(即10<15)返回true并将变量i的值增加1(由于后缀增量运算符)。循环体将i的值修改为31。第二次条件评估i++<15(即31<15)为false。但是,由于后缀增量运算符,i的值增加到32。最终打印的值是32

Q5-3.

以下哪个陈述是正确的?

  1. 增强型for循环不能在常规for循环中使用。
  2. 增强型for循环不能在while循环中使用。
  3. 增强型for循环可以在do-while循环中使用。
  4. 增强型for循环不能在switch结构中使用。
  5. 所有上述陈述都是错误的。

答案:c

解释:增强型for循环可以在所有类型的循环和条件结构中使用。注意答案选项中“可以”和“不能”的使用。注意这些细微的差异很重要。

Q5-4.

以下代码的输出是什么?

int a =  10;
if (a++ > 10) {
    System.out.println("true");
}
{
    System.out.println("false");
}
System.out.println("ABC");
  1. true
    false
    ABC
    
  2. false
    ABC
    
  3. true
    ABC
    
  4. 编译错误

答案:b

说明:首先,代码没有编译错误。这个问题有一个陷阱——以下代码片段不是 if 结构的一部分:

{
    System.out.println("false");
}

因此,无论 if 结构中的条件评估为 truefalse,都会打印 false 值。

因为这个代码片段的开闭大括号紧跟在 if 结构之后,这会让你认为这个代码片段是 if 结构的 else 部分。此外,请注意,if 结构使用关键字 else 来定义 else 部分。这个关键字在这个问题中缺失。

if 条件(即 a++ > 10)评估为 false,因为后缀增量运算符 (a++) 在其早期值被使用后立即增加变量 a 的值。10 不大于 10,因此这个条件评估为 false

Q5-5.

给定以下代码,以下哪条可选代码行可以单独替换 //INSERT CODE HERE 行,从而使代码成功编译?

class EJavaGuru {
    public static void main(String args[]) {
        int num = 10;
        final int num2 = 20;
        switch (num) {
            // INSERT CODE HERE
            break;
            default: System.out.println("default");
        }
    }
}
  1. case 10*3: System.out.println(2);
  2. case num: System.out.println(3);
  3. case 10/3: System.out.println(4);
  4. case num2: System.out.println(5);

答案:a, c, d

说明:选项 (a) 是正确的。编译时常量,包括表达式,可以在 case 标签中使用。

选项 (b) 是错误的。case 标签应该是编译时常量。非 final 变量不是编译时常量,因为它可以在类执行过程中重新赋值。尽管前面的类没有给它赋值,但编译器仍然将其视为可变的变量。

选项 (c) 是正确的。case 标签中指定的值应该可以赋给 switch 结构中使用的变量。你可能认为 10/3 会返回一个十进制数,这不能赋给变量 num,但这个操作会丢弃小数部分,并将 3 与变量 num 进行比较。

选项 (d) 是正确的。变量 num2 被定义为 final 变量,并在同一行代码中与声明一起赋值。因此,它被认为是编译时常量。

Q5-6.

以下代码的输出是什么?

class EJavaGuru {
    public static void main(String args[]) {
        int num = 20;
        final int num2;
        num2 = 20;
        switch (num) {
            default: System.out.println("default");
            case num2: System.out.println(4);
            break;
        }
    }
}
  1. default
  2. default
    4
    
  3. 4
  4. 编译错误

答案:d

说明:该代码将无法编译。case 标签需要编译时常量值,而变量 num2 不符合这一要求。尽管变量 num2 被定义为 final 变量,但它的声明中没有为其赋值。代码在声明之后将字面值 20 赋予了这个变量,但 Java 编译器不认为这是一个编译时常量。

Q5-7.

以下代码的输出是什么?

class EJavaGuru {
    public static void main(String args[]) {
        int num = 120;
        switch (num) {
            default: System.out.println("default");
            case 0: System.out.println("case1");

            case 10*2-20: System.out.println("case2");
            break;
        }
    }
}
  1. default
    case1
    case2
    
  2. case1
    case2
    
  3. case2
  4. 编译错误
  5. 运行时异常

答案:d

说明:用于两个case标签的表达式——010*2-20——都评估为常量值0。因为不能为switch语句定义重复的case标签,所以代码将无法编译,并显示错误消息,指出代码定义了重复的case标签。

Q5-8.

以下代码的输出是什么?

class EJavaGuru3 {
    public static void main(String args[]) {
        byte foo = 120;
        switch (foo) {
            default: System.out.println("ejavaguru"); break;
            case 2: System.out.println("e"); break;
            case 120: System.out.println("ejava");
            case 121: System.out.println("enum");
            case 127: System.out.println("guru"); break;
        }
    }
}
  1. ejava
    enum
    guru
    
  2. ejava
  3. ejavaguru
    e
    
  4. ejava
    enum
    guru
    ejavaguru
    

答案:a

说明:对于switch案例结构,当找到匹配的case时,控制进入case标签。然后控制会通过剩余的case标签传递,直到遇到break语句终止。当控制遇到break语句或达到switch结构的末尾时,控制退出switch结构。

在这个例子中,找到了与case标签120匹配的标签。控制执行这个case标签的语句,并将ejava打印到控制台。因为break语句没有终止这个case标签,所以控制会传递到case标签121。控制执行这个case标签的语句,并将enum打印到控制台。因为break语句也没有终止这个case标签,所以控制会传递到case标签127。控制执行这个case标签的语句,并将guru打印到控制台。这个case标签通过break语句终止,所以控制退出switch结构。

Q5-9.

以下代码的输出是什么?

class EJavaGuru4 {
    public static void main(String args[]) {
        boolean myVal = false;
        if (myVal=true)
        for (int i = 0; i < 2; i++) System.out.println(i);
        else System.out.println("else");
    }
}
  1. else
  2. 0
    1
    2
    
  3. 0
    1
    
  4. 编译错误

答案:c

说明:首先,if结构中使用的表达式并不是比较变量myVal的值与字面值true,而是将字面值true赋值给它。赋值运算符(=)用于赋值。比较运算符(==)用于比较值。因为结果值是一个boolean值,编译器不会在if结构中的赋值上发出抱怨。

代码故意缩进不良,因为你在 OCA Java SE 8 程序员 I 考试中可能会遇到类似的糟糕缩进。for循环是if结构的一部分,打印01else部分不执行,因为if条件评估为true。代码没有编译错误。

Q5-10.

以下代码的输出是什么?

class EJavaGuru5 {
    public static void main(String args[]) {
        int i = 0;
        for (; i < 2; i=i+5) {
            if (i < 5) continue;
            System.out.println(i);
        }
        System.out.println(i);
    }
}
  1. 编译错误
  2. 0
    5
    
  3. 0
    5
    10
    
  4. 10
  5. 0
    1
    5
    
  6. 5

答案:f

说明:首先,以下代码行没有编译错误:

for (; i < 2; i=i+5) {

for循环中使用初始化块是可选的。在这种情况下,使用分号(;)来终止它。

对于第一次for迭代,变量i的值为0。因为这个值小于2,所以以下if结构评估为true,并执行continue语句:

if (i < 5) continue;

因为continue语句忽略了for循环迭代中剩余的所有语句,所以控制台不会打印变量i的值,这导致控制台跳转到下一个for迭代。在下一个for迭代中,变量i的值为5for循环条件评估为false,控制台退出for循环。在for循环之后,代码打印出变量i的值,使用代码i=i+5将其增加一次。

第六章。处理继承

本章涵盖的考试目标 你需要了解的内容
[7.1] 描述继承及其好处。 需要继承类的原因。如何使用类实现继承。
[7.2] 开发演示多态使用的代码;包括重写和对象类型与引用类型。 如何使用类和接口实现多态。如何定义多态或重写方法。如何确定可以用来引用对象的变量的有效类型。如何确定对象的成员之间的差异,哪些是可访问的,以及当使用继承的基类或实现的接口的变量引用对象时会发生什么。
[7.3] 确定何时需要类型转换。 类型转换的需要。如何将对象转换到另一个类或接口。
[7.4] 使用 super 和 this 来访问对象和构造函数。 如何使用 super 和 this 访问变量、方法和构造函数。如果派生类尝试访问对派生类不可访问的基类变量会发生什么。
[7.5] 使用抽象类和接口。 抽象类和接口在实现多态中的作用。
[9.5] 编写一个简单的 Lambda 表达式,该表达式消耗一个 Lambda 谓词表达式 Lambda 表达式的语法和用法。Predicate 类的使用。

所有生物都继承了其父母的特征和行为。苍蝇的后代看起来像苍蝇,狮子后代看起来像狮子。但尽管与父母相似,所有后代都以自己的方式不同和独特。此外,单个动作对不同生物可能有不同的含义。例如,“吃”这个动作对苍蝇和狮子有不同的含义。苍蝇吃花蜜,而狮子吃羚羊。

类似的情况也发生在 Java 中。从父类继承特性和行为的概念可以与类从父类继承变量和方法相比较。以独特的方式与众不同类似于一个类可以同时从父类继承并定义额外的变量和方法。单个动作具有不同含义的情况可以与 Java 中的多态性相比较。

在 OCA Java SE 8 程序员 I 考试中,你将需要回答有关如何实现继承和多态以及如何使用类和接口的问题。因此,本章涵盖了以下内容:

  • 理解和实现继承

  • 开发演示多态使用的代码

  • 区分引用类型和对象类型

  • 确定何时需要类型转换

  • 使用superthis来访问对象和构造函数

  • 使用抽象类和接口

6.1. 类的继承

[7.1] 描述继承及其好处

[7.5] 使用抽象类和接口

当我们在面向对象编程语言(如 Java)的上下文中讨论继承时,我们谈论的是一个类如何继承另一个类的属性和行为。从另一个类继承的类也可以定义额外的属性和行为。考试将明确询问继承类的需要以及如何使用类实现继承。

让我们从需要继承类的情况开始。

6.1.1. 继承类的需要

想象一个组织中的程序员经理职位。这两个职位都有一个共同的属性集合,包括姓名、地址和电话号码。这些职位也有不同的属性。程序员可能关注项目的编程语言,而经理可能关注项目状态报告。

假设你需要在办公室存储所有程序员和管理员的详细信息。图 6.1 显示了你可能识别出的程序员和管理员的属性和行为,以及它们作为类的表示。

图 6.1. 程序员和管理员的属性和行为,以及它们作为类的表示

你注意到程序员和管理员类有共同属性,即nameaddressphoneNumberexperience吗?下一步是将这些共同属性提取到新的位置,并命名为类似Employee的东西。这一步在图 6.2 中显示。

图 6.2. 识别程序员和管理员的共同属性和行为,将它们提取到新的位置,并将其命名为 Employee。

这个新位置,Employee,可以被定义为一个新的类Employee,它被ProgrammerManager类继承。一个类使用关键字extends继承一个类,如图 6.3 所示。

图 6.3. ProgrammerManager类扩展了Employee类。

继承一个类也被称为子类化。在图 6.3 中,继承的类Employee也被称为超类基类父类。继承Employee类的ProgrammerManager类被称为子类派生类扩展类子类

你认为为什么需要将共同属性和行为提取到单独的类Employee中,并使ProgrammerManager类继承它?下一节将介绍继承类的益处。

6.1.2. 益处

你知道在 Java 中,所有类都隐式或显式地继承自 java.lang.Object 类吗?扩展一个类提供了多个好处。让我们回顾上一节中使用的示例,以突出继承类的优势。

较小的派生类定义

如果你需要编写更专业的类,例如具有与 Employee 类相同常见特性和行为的 AstronautDoctor 类,会发生什么?有了 Employee 类,你只需定义 AstronautDoctor 类特有的变量和方法,并让这些类继承 Employee

图 6.4 是 AstronautDoctorProgrammerManager 类的 UML 表示,包括从 Employee 类继承和不继承的情况。如图所示,当这些类继承自 Employee 类时,它们的定义更小。

图 6.4. AstronautDoctorProgrammerManager 类的大小差异,包括和不包括从 Employee 类继承

注意

本书使用的示例被简化和泛化,以便你可以专注于正在覆盖的概念。它们没有考虑所有现实世界的场景。例如,在某个特定项目中,宇航员或医生可能不是某个组织的雇员。

修改公共属性和行为容易

如果你的老板介入并告诉你,所有这些专业类——AstronautDoctorProgrammerManager——现在都应该有一个属性 facebookId,会发生什么?图 6.5 显示,在基类 Employee 存在的情况下,你只需将这个变量添加到基类中。如果你没有从 Employee 类继承,你需要将变量 facebookId 添加到这四个类中的每一个。

图 6.5. 向所有类添加新的属性 facebookId,包括和不包括基类 Employee

注意,可以从基类 Employee 中相对容易地修改和删除公共代码。

可扩展性

与层次树中的基类一起工作的代码可以与后来使用继承添加的所有类一起工作。

假设一个组织需要向所有员工发送邀请,并且它使用以下方法来这样做:

class HR {
    void sendInvitation(Employee emp) {
        System.out.println("Send invitation to" +
                                     emp.name + " at " + emp.address);
    }
}

因为 sendInvitation 方法接受类型为 Employee 的参数,所以你也可以传递一个 Employee 的子类给它。本质上,这种设计意味着你可以使用之前的方法与后来定义的具有 Employee 作为其基类的类一起使用。继承使得代码可扩展。

使用基类中的经过验证的代码

你不需要重新发明轮子。有了继承,子类可以使用基类中的经过验证的代码。

专注于你类的专业行为

继承一个类使你能够专注于定义你类特殊行为的变量和方法。继承让你能够使用基类中已经定义的现有代码,而无需自己定义它。

逻辑结构和分组

当多个类继承一个基类时,这会创建一个逻辑组。例如,请参阅图 6.5。类AstronautDoctorProgrammerManager都被分组为类Employee的类型。

考试技巧

继承使你能够重用已经由类定义的代码。继承可以通过扩展一个类来实现。

下一个部分将解决如何在派生类中直接访问基类继承成员的神秘。

6.1.3. 派生类在其内部包含其基类的对象

ProgrammerManager继承类Employee中定义的非私有变量和方法,并直接使用它们,就像它们是在它们自己的类中定义的一样。检查以下代码:

图片

如何让类Programmer为在类Employee中定义的变量赋值?你可以这样考虑这种安排:当一个类继承另一个类时,它在其内部封装了一个继承类的对象。因此,继承类的所有非私有成员(变量和方法)都对类可用,如图 6.6 所示。

图 6.6. 派生类的对象可以访问其基类对象的功能。

图片

但是派生类不能继承其基类的所有成员。接下来的两个部分将讨论哪些基类成员被派生类继承,哪些不被继承。

6.1.4. 哪些基类成员被派生类继承?

访问修饰符在确定派生类中基类成员的继承中起着重要作用。派生类只能继承它能看到的。派生类继承其基类的所有非私有成员。派生类继承具有以下可访问级别的基类成员:

  • 默认—只有当基类和派生类位于同一包中时,具有默认访问权限的成员才可以在派生类中访问。

  • protected—具有protected访问权限的成员对所有派生类都是可访问的,无论基类和派生类定义在哪个包中。

  • public—具有public访问权限的成员对所有其他类都是可见的。

考试技巧

派生类只能继承它能看到的。

6.1.5. 哪些基类成员不被派生类继承?

派生类不继承以下成员:

  • 基类的private成员。

  • 如果基类和派生类存在于不同的包中,则具有默认访问权限的基类成员。

  • 基类构造函数。派生类可以调用基类的构造函数,但它并不继承它们(第 6.5 节 讨论了派生类如何使用隐式引用 super 调用基类的构造函数)。

除了继承其基类的属性和行为外,派生类还可以定义额外的属性和行为,如下一节所述。

6.1.6. 派生类可以定义额外的属性和行为

尽管派生类与它们的基类相似,但它们通常也有差异。派生类可以定义额外的属性和行为。您可能在考试中看到关于派生类如何与其基类不同的明确问题。

快速回顾一下 图 6.5。所有派生类——ManagerProgrammerDoctorAstronaut——都定义了额外的变量、方法或两者兼而有之。派生类还可以定义自己的构造函数和 static 方法以及变量。派生类还可以 隐藏重写 它的基类成员。

当派生类定义了一个与基类中定义的名称相同的实例或类变量时,只有这些新变量和方法对使用派生类的代码可见。当派生类通过重新定义方法来为从基类继承的方法定义不同的代码时,此方法被视为一个特殊方法——一个 重写 方法。

您可以通过使用具体类或 abstract 类作为基类来实现继承,但您应该注意一些重要的区别。这些将在下一节中讨论。

6.1.7. 抽象基类与具体基类

图 6.2 和 6.3 展示了如何提取 ProgrammerManager 的公共属性和行为,并将这些表示为一个新的类,Employee。如果您认为它只是一个分类,而在现实生活中并没有真正的 Employee 存在——也就是说,如果所有员工实际上都是 ProgrammerManager,那么您可以将 Employee 类定义为 abstract 类。这就是 abstract 类的本质:它将派生类的公共属性和行为分组,但它阻止自己被实例化。此外,一个 abstract 类可以通过将其定义为 abstract 方法(一个没有主体的方法)来 强制 所有派生类为其行为定义自己的实现。

注意

第 6.6.1 节 包含了抽象类使用示例:它如何强制其派生类实现 abstract 方法。

abstract类不一定要定义abstract方法。但如果abstract基类定义了一个或多个abstract方法,该类必须被标记为abstract,并且所有具体的派生类都必须实现这些abstract方法。如果一个派生类没有实现其基类定义的所有abstract方法,那么它也需要是一个abstract类。

对于考试,你需要记住使用abstract基类实现继承的重要要点:

  • 你永远不能创建abstract类的对象。

  • 基类可以定义为abstract类,即使它没有定义任何abstract方法。

  • 派生类应该实现其基类中所有的abstract方法。如果不实现,它必须被定义为abstract派生类。

  • 你可以使用abstract基类的变量来引用其派生类的对象(详细讨论见第 6.3 节)。

本章的第一个故事转折练习询问你关于基类和派生类之间的关系(答案见附录)。

故事转折 6.1

修改之前例子中使用的代码如下。以下哪个选项是修改后代码的正确选项?

class Employee {
    private String name;
    String address;
    protected String phoneNumber;
    public float experience;
}
class Programmer extends Employee {
    Programmer (String val) {
        name = val;
    }
    String getName() {
        return name;
    }
}
class Office {
    public static void main(String args[]) {
        new Programmer ("Harry").getName();
    }
}
  1. Office类打印出Harry

  2. 派生类Programmer不能为其基类Employee中定义的变量定义一个 getter 方法。

  3. 派生类Programmer在其构造函数中不能访问其基类Employee中的变量-。

  4. new Programmer ("Harry").getName(); 并不是创建Programmer类对象的正确方式。

  5. 编译错误。

要记住的术语和定义

以下是一份你应该记住的术语及其对应定义的列表;它们贯穿整个章节,你将在回答 OCA Java SE 8 程序员 I 考试中关于继承的问题时遇到它们。

  • 基类—被另一个类继承的类。在之前的例子中,EmployeeProgrammerManager基类

    • 超类—一个基类也被称为超类

    • 父类—基类也被称为父类

  • 派生类—从另一个类继承的类。在之前的例子中,ProgrammerManager派生类

    • 子类—派生类也被称为子类

    • 扩展类—派生类也被称为扩展类

    • 子类—派生类也被称为子类

  • “是”关系—基类和派生类之间共享的关系。在之前的例子中,ProgrammerEmployee的“是”。ManagerEmployee的“是”。因为派生类代表基类的一种特殊类型,所以派生类基类的一种。

  • extends—类用来继承另一个类,接口用来继承另一个接口的关键字。

  • implements—类用来实现接口的关键字(接口将在下一节中介绍)。

注意

术语base classsuperclassparent class可以互换使用。同样,术语derived classsubclass也可以互换使用。

在本节中,你了解到一个abstract类可以定义abstract方法。让我们更进一步,讨论接口。在下一节中,我们将讨论为什么需要接口以及如何使用它们。

6.2. 使用接口

[7.1] 描述继承及其优点

[7.5] 使用抽象类和接口

我们在生活中经常使用接口。例如,当你称呼某人为runner时,你是否关心那个人是否也是一位演说家、一个家长或一个企业家?你只关心那个人是否能够run。术语runner使你能够通过为每个人打开一个小窗口来访问只适用于那个人作为跑步者能力的操作。只有当那个人支持与跑步相关的特征时,那个人才能被称为跑步者,尽管具体的行为可能取决于个人。

在前面的例子中,你可以将术语runner与 Java 接口进行比较,该接口定义了所需的操作run。接口可以定义一组行为(方法)和常量。通常,它将行为的实现委托给实现它的类。接口用于引用具有相同行为集的多个相关或不相关的对象。图 6.7 将接口runner与一个小的窗口与一个对象进行比较,该对象只关注该对象的运行能力。

图 6.7。你可以将一个可以连接多个对象但对其访问有限的用户界面与一个窗口进行比较。

图 6.7

同样,当你使用接口设计应用程序时,你可以使用类似的窗口(也称为specificationscontracts)来指定你需要从对象中获取的行为,而不关心对象的特定类型。

注意

你可以将contract与一组由人员相互接受的规定或可交付成果进行比较。合同可能包括一组必须遵守的规定或必须在特定日期前提供的可交付成果。合同通常不包括如何遵守所陈述的规定或如何使可交付成果可访问。它只说明什么,而不说明如何。同样,接口定义了由实现它的类支持的什么行为。

将所需行为与其实现分离有许多好处。作为一个应用程序设计者,你可以使用接口来建立从对象中所需的行为,从而提高设计的灵活性(可以创建并稍后使用实现接口的新类)。接口使应用程序易于管理、可扩展,并减少由于现有类型更改而传播错误的倾向。

现在想象你之前在应用程序中创建了一个接口。应用程序需要升级,这需要向其一些接口添加额外的行为。在 Java 7 或其早期版本中,这是不可能的。但是,随着 Java 8 的推出,你可以在不破坏现有实现的情况下向接口添加方法。在 Java 8 之前,接口只能定义抽象方法。随着 Java 8 的推出,接口可以为其方法定义默认实现(这样就不会阻止实现它的现有类编译)。Java 8 中的接口还可以定义静态方法。这种语言变化(向接口添加默认和静态方法)的主要原因之一是为了改进过时的 Collections API,特别是基于 Stream 的功能(由 OCP 考试涵盖)。

在本节中,你将了解使用接口的需求和重要性,以及可以在接口中定义的不同类型的方法。你将处理接口成员的隐式和显式属性——它的常量和方法。你还将看到为什么不允许继承多个类,但允许继承多个接口。让我们从接口的需求开始。

6.2.1. 使用接口的需求

你需要接口来使多个类能够支持一组行为。让我们以第 6.1 节中使用的示例为例。在这个例子中,Employee是基类,ProgrammerManager类是Employee的子类。想象一下,你的老板介入并声明ProgrammerManager必须支持额外的行为,如表 6.1 中列出。

表 6.1. 需要由ProgrammerManager类支持的其他行为
实体 新的预期行为
程序员 参加培训
经理 参加培训,进行面试

你将如何完成这项任务?你可以采取的一种方法是在Employee类中定义所有相关的方法。因为ProgrammerManager都扩展了Employee类,所以它们将能够访问这些方法。但是等等:Programmer不需要进行面试的行为;只有Manager应该支持进行面试的功能。

另一种明显的方法是在所需的类中定义相关的方法。你可以在 Manager 类中定义面试方法,在 ProgrammerManager 类中定义参加培训的方法。再次强调,这并不是一个理想解决方案。如果你的老板后来告诉你,所有参加培训的 Employee 都应该接受一个培训计划;也就是说,定义“参加培训”行为的方法签名发生了变化?你能为这种行为定义单独的类,并让 ProgrammerManager 类实现它们吗?不,你不能。Java 不允许一个类继承多个类(本章后面的部分将介绍)。

让我们尝试接口。创建两个接口来定义指定的行为:

interface Trainable {
    public void attendTraining();
}

interface Interviewer {
    public void conductInterview();
}

虽然 Java 不允许一个类从多个类中继承,但它允许一个类实现多个接口。一个类使用 implements 关键字来实现接口。在下面的代码中,ProgrammerManager 类实现了相关的接口(修改后的代码用粗体表示):

图片

图 6.8 使用 UML 图显示了这些类之间的关系。

图 6.8. EmployeeProgrammerManager 类以及 TrainableInterviewer 接口之间的关系

图片

注意

在 UML 图中,可以使用带有文本 <> 的矩形或简单地使用一个圆来表示接口。这两种表示法都很流行;你可能会在各种网站或书籍中看到它们。

前面的关系也可以表示如图 6.9 所示,其中接口被定义为圆。

图 6.9. EmployeeProgrammerManager 类以及 TrainableInterviewer 接口之间的关系,接口用圆表示

图片

6.2.2. 定义接口

你可以在接口中定义方法和常量。声明接口很简单,但不要让这种简单性误导你。对于考试来说,了解添加到接口成员中的隐式修饰符非常重要。接口的所有方法都是隐式 public 的。接口变量是隐式 publicstaticfinal 的。让我们定义一个名为 Runner 的接口,它定义了一个 abstract 方法 speed 和一个变量 distance。图 6.10 展示了在编译过程中如何向接口 Runner 的成员添加隐式修饰符。

图 6.10. 接口的所有方法都是隐式公共的。它的变量是隐式公共的、静态的和最终的。

图片

你认为为什么这些隐式修饰符被添加到接口成员中?因为接口用于定义契约,限制对其成员的访问是没有意义的——因此它们隐式为公共。接口不能被实例化,因此其变量的值应在静态上下文中定义和访问,这使得它们隐式为静态。

考试还将测试你对接口声明各个组件的了解,包括访问和非访问修饰符。以下是接口声明组件的完整列表:

  • 访问修饰符

  • 非访问修饰符

  • 接口名称

  • 如果接口扩展了任何接口,则所有扩展的接口

  • 接口体(变量和方法),包含在一对花括号 {}

要包含所有可能的组件,让我们修改接口 Runner 的声明:

public strictfp interface Runner extends Athlete, Walker {}

接口 Runner 的组件在 图 6.11 中展示。要声明任何接口,必须 包含关键字 interface、接口名称以及其体,由 {} 标记。

图 6.11. 接口声明的组件

图片

接口的可选和必选组件可以总结如下,见 表 6.2。

表 6.2. 接口声明的可选和必选组件
必选 可选
关键字 interface 访问修饰符
接口名称 非访问修饰符
接口体,由开闭花括号 {} 标记 关键字 extends,以及基接口的名称(与类不同,接口可以扩展多个接口。)
考试技巧

接口声明不能包含类名。接口永远不会扩展任何类。

你能定义一个顶级、受保护的接口吗?不,你不能。对于考试,你必须知道关于接口声明中每个组件正确值的答案。让我们深入了解这些细微差别。

接口的有效访问修饰符

你可以声明一个 顶级接口(不在任何其他类或接口中声明的接口),仅以下访问级别:

  • public

  • 无修饰符(默认访问)

如果你尝试使用其他访问修饰符(protectedprivate)声明顶级接口,你的接口将无法编译。以下 MyInterface 接口的定义将无法编译:

图片

考试技巧

所有的顶级 Java 类型(类、枚举和接口)只能使用两种访问级别声明:public 和默认。内部或嵌套类型可以使用任何访问级别声明。

接口成员的有效访问修饰符

接口的所有成员——变量、方法、内部接口和内部类(是的,一个接口可以在其中定义一个类!)——都是隐式公共的,因为这是它们唯一可以接受的修饰符。使用其他访问修饰符会导致编译错误:

代码在 处编译失败,错误信息如下:

illegal combination of modifiers: public and private
    private int number = 10;
接口的有效非访问修饰符

你只能使用以下非访问修饰符声明顶级接口:

  • abstract

  • strictfp

注意

strictfp关键字确保所有平台上的所有浮点计算结果都是相同的。

如果你尝试使用其他非访问修饰符(finalstatictransientsynchronizedvolatile)来声明顶级接口,该接口将无法编译。以下所有接口声明都无法编译,因为它们使用了无效的非访问修饰符:

6.2.3. 接口中的方法类型

Oracle 对 Java 8 中的接口进行了根本性的改变。除了abstract方法外,接口还可以定义具有默认实现的方法。它还可以定义static方法。以下是一个快速列表,列出了可以在接口中定义的方法类型(在 Java 8 中):

  • abstract 方法

  • 默认方法(Java 8 新增)

  • static 方法(Java 8 新增)

让我们逐一详细考察这些内容。

注意

默认方法也被称为防御者虚拟扩展方法。但最常用的术语来指代它们是默认方法,因为使用了default关键字来识别它们。

抽象方法

大多数工作都需要候选人接受面试,面试官可以是 CEO、技术领导或程序员。尽管这些类别中的每一个都支持面试官的行为,但它们将以自己的特定方式conductInterview

一个abstract方法用于指定一种行为(方法集),它必须由实现它的类定义。这是另一种表达“一个类支持一种行为,但以它喜欢的方式”的方式。在以下示例中,接口Interviewer定义了一个abstract方法conductInterview

一个abstract方法没有方法体:

你可以在接口中包含关键字abstract来定义一个abstract方法。以下对方法conductInterview的定义与前面代码中的定义相同:

考试技巧

接口方法隐式地是abstract的。要定义默认静态方法,你必须显式地在接口中用defaultstatic关键字声明方法。默认和static方法在接口中包含它们的实现。

当一个类实现包含abstract方法的一个接口时,该类必须实现所有方法,否则类无法编译。开发者不能在不破坏现有实现的情况下向接口添加abstract方法。这只能通过默认方法来实现。

默认方法

想象一下,你需要在接口Interviewer发布后添加一个行为——提交面试状态。在没有 Java 7 及其早期版本的情况下,这不可能实现,除非需要为每个现有的具体类提供实现(直接或通过超类)。默认方法可以在这里救你。从 Java 8 开始,接口可以通过添加具有默认实现的方法来扩展。实现类可以选择覆盖这些方法以定义它们自己的特定行为。如果它们选择不覆盖它们,则使用接口中的默认实现。默认方法的定义必须包含关键字default

我故意在前面的代码中简化了submitInterviewStatus(),以便代码专注于默认方法的定义,而不是其实现细节。

Java 8 新增功能

接口方法可以使用default方法定义一个实现。

因为前一个方法submitInterviewStatus的返回类型是void,所以以下对方法submitInterviewStatus的定义是有效的:

尽管前一个示例中的方法在其主体中未定义任何代码,但它并不等同于一个abstract方法。默认方法的声明必须后跟使用{}标记的方法体。以下代码无法编译:

正如常规方法一样,默认方法的返回类型必须与它返回的值的类型匹配。以下代码无法编译:

静态方法

回顾前一个章节中使用的Interviewer接口。想象一下,你需要一个实用(静态)方法,可以在特定日期和时间预订面试的会议室。在 Java 8 中,你可以向接口添加static方法。在 Java 8 之前,接口不允许定义static方法。在这种情况下,你需要在一个单独的类中定义所需的static方法。这是允许在接口中使用static方法的主要原因之一——为了改进过时的 Collections API,该 API 包括一些仅用于定义static方法的类(如CollectionsPaths)。

注意

static接口方法允许你在它们所属的接口中定义实用方法。

让我们在接口Interviewer(加粗)中添加一个static方法bookConferenceRoom

interface Interviewer {
    abstract void conductInterview();
    default void submitInterviewStatus() {
        System.out.println("Accept");
    }
    static void bookConferenceRoom(LocalDateTime dateTime, int duration) {
        System.out.println("Interview scheduled on:" + dateTime);
        System.out.println("Book conference room for: "+duration + " hrs");
    }
}

方法bookConferenceRoom()必须通过在调用前加上接口名称来调用。你不能使用类型为Interviewer或实现此接口的类的引用变量来调用它。让我们定义实现Interviewer接口的类Manager和尝试调用bookConferenceRoom方法的类Project

404fig01_alt.jpg

有趣的是,对于mgr.bookConferenceRoom(),编译器指出bookConferenceRoom方法在Manager类型中未定义。

考试技巧

接口中的static方法不能使用引用变量调用。它必须使用接口名称来调用。

与前面的代码相比,你可以通过使用引用变量或类名来调用在类中定义的static方法:

404fig02_alt.jpg

在前面的例子中,static方法defaultPlan是在Employee类中定义的,该类是Programmer类的子类。Project类定义了类型为EmployeeProgrammer的引用变量,并使用Programmer实例初始化它们。要执行defaultPlan(),你可以使用类型为EmployeeProgrammer的引用变量emppgr,或者使用类名:EmployeeProgrammer

考试技巧

与接口不同,如果你在基类中定义一个static方法,它可以通过引用变量或类名来访问。

6.2.4. 实现单个接口

当一个类实现一个接口时,它必须遵循一系列规则。

实现抽象方法

如果一个具体类没有实现它所实现的接口中的abstract方法,则无法编译:

405fig01_alt.jpg

你认为以下代码会编译吗?

405fig02_alt.jpg

考试技巧

你必须使用显式的访问修饰符public来实现接口的abstract方法。

当你在类中实现接口方法时,它遵循方法重写规则:

405fig03_alt.jpg

但以下代码无法编译:

405fig04_alt.jpg

重写默认方法

一个类可能会选择重写它所实现的接口中的默认方法。如果不这样做,将使用接口方法的默认实现。在以下示例中,Manager类实现了Interviewer接口,但没有重写默认方法submitInterview-Status()

406fig01_alt.jpg

让我们在Manager类中重写submitInterviewStatus()方法的默认实现。当一个类重写默认方法时,它不使用default关键字。同时,它遵循方法重写规则:

406fig02_alt.jpg

考试技巧

在重写默认方法时,你不可以使用default关键字。重写默认方法和常规方法的规则相同。

静态方法

如果一个接口定义了一个static方法,实现它的类可以定义一个具有相同名称的static方法,但接口中的方法与类中定义的方法无关。在以下示例中,Manager类中的bookConference-Room方法没有重载或覆盖在Interviewer接口中定义的bookConference-Room方法。这从这些方法的返回类型(加粗显示)中可以看出:

406fig03_alt.jpg

考试技巧

类中的static方法和它实现的接口之间没有关系。类中的static方法不会隐藏或覆盖它实现的接口中的static方法。

你认为为什么 Java 不允许一个类继承多个类,却允许一个类实现多个接口?我将在下一节详细说明。

6.2.5. 一个类不能扩展多个类

在 Java 中,一个类不能扩展多个类。让我们通过一个例子来检查原因,其中类Programmer可以继承两个类:EmployeePhilanthropist。图 6.12 显示了这些类之间的关系以及相应的代码。

图 6.12. 如果允许一个类扩展多个类会发生什么?

06fig12_alt.jpg

如果Programmer类继承了EmployeePhilanthropist中定义的receiveSalary方法,你认为Programmer会如何处理他们的薪水:缴纳会费(像Employee一样)还是捐赠(像Philanthropist一样)?你认为以下代码的输出会是什么?

407fig01_alt.jpg

在这种情况下,Programmer类可以访问两个具有相同方法签名但不同实现的receiveSalary方法,因此无法解析这个方法调用。这就是为什么 Java 不允许类继承多个类的原因。

考试技巧

因为派生类可能从多个基类继承相同方法签名的不同实现,Java 不允许多重继承。

6.2.6. 一个类可以实现多个接口

在前面的章节中,我们讨论了类不能继承多个类。但一个类可以实现多个接口。为什么 Java 不允许一个类继承多个类,却允许一个类实现多个接口呢?在 Java 8 之前,接口只能定义抽象方法。所以即使一个类从不同的接口继承同名方法,它也没有实现。

但在 Java 8 中,接口也可以定义默认方法——包含实现的方法。因此,当一个类实现多个接口时,它必须遵守一组规则。

考试技巧

只有当遵守一组规则时,一个类才能扩展多个接口。

实现具有相同常量名称的多个接口

一个类可以实现具有相同常量名称的多个接口,只要对这些接口的调用不是模糊的。在以下示例中,类Animal可以成功编译。它没有使用它实现的接口MoveableJumpable中定义的常量MIN_DISTANCE

图片

如果你修改了类Animal的实现细节,使其引用变量MIN_DISTANCE而不在前面加上接口名称,那么它将无法编译:

图片

当用接口名称作为前缀时,对MIN_DISTANCE的引用就不再模糊了:

图片

如果对接口(s)中定义的常量的隐式引用不是模糊的,实现接口的类可以引用它,而不需要在前面加上接口名称:

图片

考试技巧

一个类可以实现具有相同常量名称的多个接口,仅当对常量的引用不是模糊的时候。

实现具有相同抽象方法名称的多个接口

一个abstract方法不定义一个主体。一个类可以扩展多个定义具有相同签名的abstract方法的接口是可接受的,因为当一个类实现abstract方法时,它似乎实现了所有接口的abstract方法:

interface Jumpable {
    abstract String currentPosition();
}
interface Moveable {
    abstract String currentPosition();
}
class Animal implements Jumpable, Moveable {
    public String currentPosition() {
        return "Home";
    }
}

但是,你不能使一个类扩展多个定义具有相同名称的方法的接口,这些方法似乎不是正确的重载方法组合。如果你将接口MoveablecurrentPosition()方法的返回类型从String更改为void,类Animal将无法编译。它需要实现返回类型不同的currentPosition方法,这是不可接受的:

图片

考试技巧

如果具有相同的签名或形成一组重载方法,一个类可以实现具有相同abstract方法名称的多个接口。

实现具有相同默认方法名称的多个接口

想象一个名为Animal的类,它扩展了多个接口MoveableJumpable,这些接口定义了具有相同名称relax()的默认方法。如果类Animal没有覆盖relax()的默认实现,它将无法编译:

图片

让我们修改前面的代码,以便类Animal覆盖relax()的默认实现。在这种情况下,它将成功编译:

图片

一个类从它实现的接口继承的默认方法必须形成一个正确的重载方法集,否则该类将无法编译:

图片

考试技巧

如果一个类覆盖了它的默认实现,它可以实现具有相同默认方法名称和签名的多个接口。

实现具有相同静态方法名称的多个接口

一个类可以实现定义了具有相同名称的 static 方法的多个接口,即使它们不符合正确重载或覆盖方法的条件。这是因为它们不会被实现接口的类继承:

考试提示

一个类可以实现具有相同 static 方法名称的多个接口,无论它们的返回类型或签名如何。

6.2.7. 扩展接口

一个接口可以扩展多个接口。当一个接口扩展另一个接口时,它必须遵循一组规则。

扩展具有相同抽象方法名称的多个接口

一个 abstract 方法没有定义方法体。考虑以下代码,其 UML 表示形式如图 6.13 所示。接口 MyInterface 将继承哪个 getName 方法?MyInterface 将继承 BaseInterface1 中定义的 getName 方法还是 BaseInterface2 中定义的?

图 6.13. 接口 MyInterface 扩展了接口 BaseInterface1BaseInterface2

interface BaseInterface1 {
    String getName();
}
interface BaseInterface2 {
    String getName();
}
interface MyInterface extends BaseInterface1, BaseInterface2 {}

因为 BaseInterface1Base-Interface2 中定义的 getName 方法都没有定义方法体(如图 6.14 所示),所以 MyInterface 继承哪个方法的问题是不相关的。接口 MyInterface 只能访问一个 getName 方法,所有实现 MyInterface 的具体类都必须实现此方法。

图 6.14. 接口中定义的方法没有方法体。

让我们使 Employee 类实现接口 MyInterface,如下所示:

扩展具有相同名称默认方法名称的多个接口

当一个接口扩展多个接口时,Java 确保它不应该为同一个方法继承多个方法实现。在下面的示例中,接口 MyInterface 无法编译,因为它从 BaseInterface1BaseInterface2 类型中继承了与 getName() 无关的默认值:

如果你覆盖了 MyInterfacegetName() 方法的默认实现,它将能够成功编译:

在前面的代码中,MyInterface 中的 getName 方法可以通过使用 super 关键字来引用超接口方法:

  • BaseInterface1.super.getName();

  • BaseInterface2.super.getName();(如果 MyInterface 是同时实现这两个接口的类,这也会起作用。)其他方法也可以用这种方式调用超接口方法。

在多重继承的情况下,这里有三个解决规则:

  • 类总是优先:一个在类中实现的方法总是比接口默认方法有优先级。

  • 否则,子接口总是优先:在更具体的接口中实现的方法比在更通用的接口(例如,超接口)中定义的方法有优先级。

  • 否则,如果存在无法通过先前规则解决的歧义,那么你将遇到之前提到的案例:必须使用super关键字指定目标超接口。

考试技巧

当一个接口扩展多个接口时,Java 确保它不应该为同一方法继承多个方法实现。

使用相同静态方法名称扩展多个接口

接口可以扩展具有相同静态方法名称的多个接口:

图片

以下代码也可以成功编译,尽管BaseInterface1BaseInterface2status()方法的返回类型不相关:

图片

考试技巧

接口可以扩展多个接口,这些接口定义了具有相同名称的static方法;这些方法的签名并不重要。这是因为static方法永远不会被继承,因此不会发生冲突。

6.2.8. 修改接口的现有方法

如果你修改接口中方法的声明会发生什么?因为你可以在一个接口中定义多种类型的方法——abstract、默认和static——这些修改将具有不同的影响。

修改接口的现有方法可能会破坏实现它的类或扩展它的接口的代码。这些修改必须遵循实现或扩展接口的规则,如前几节详细所述。

在本节中,你将看到当你通过更改接口中方法的类型(abstract、默认或static)来修改接口时会发生什么。这种更改可能会影响实现接口的类或调用修改后方法的代码。

将静态方法更改为默认或抽象

在接口中,如果你将static方法更改为默认方法,实现类将继续编译,但调用该方法的代码将无法编译。如果你将static方法更改为abstract方法,实现类可能无法编译。代码、更改和结果如图 6.15 所示。

图 6.15. 当你将接口中的static方法更改为默认或abstract方法时会发生什么

图片

将抽象方法更改为默认或静态

如果你通过将接口的abstract方法更改为默认方法来修改接口,调用该方法的代码将继续编译。但如果你将abstract方法更改为static方法,调用该方法的代码将无法编译。这是因为接口的static方法是通过在方法名称前加上接口名称来调用的。代码、修改及其结果如图 6.16 所示。

图 6.16. 当你将接口中的abstract方法更改为默认或static方法时会发生什么

图片

将默认方法更改为抽象或静态

如果你修改了一个接口并将其默认方法更改为abstract方法,实现它的类可能会编译失败。如果实现类没有覆盖接口的默认方法,它将无法编译。如果你将接口的默认方法更改为static方法,调用该方法的代码将无法编译。代码、更改和结果在图 6.17 中显示。

图 6.17. 当你将接口中的默认方法更改为abstractstatic方法时会发生什么

6.2.9. 接口成员的属性

接口可以定义常量和方法,这些方法隐式地分配了一组属性。

接口常量

正如你已经看到的,接口的变量隐式地是publicfinalstatic。所以以下接口MyInterface的定义

interface MyInterface {
    int age = 10;
}

等价于以下定义:

你必须在接口中初始化所有变量,否则你的代码将无法编译:

接口方法

接口的方法隐式地是public的。当你实现一个接口时,你必须使用public访问修饰符来实现所有它的方法。实现接口的类不能使接口的方法更加限制性。尽管以下类和接口的定义看起来是可以接受的,但它们并不正确:

以下代码是正确的,并且可以顺利编译:

接口构造函数

与类不同,接口不能定义构造函数。

你可以使用基类的引用变量来引用其派生类的对象。同样,你也可以使用实现它的类的引用变量来引用该类的对象。值得注意的是,这些变量不能访问在派生类或实现接口的类中定义的所有变量和方法。

让我们在下一节中深入探讨这个问题的更多细节。

6.3. 引用变量和对象类型

[7.2] 编写代码以演示多态的使用;包括重写和对象类型与引用类型

对于这个考试目标,你需要理解当你引用一个对象时,对象引用变量的类型和被引用的对象的类型可能不同。但有一些规则决定了它们可以有多大的不同。这个概念可能需要一段时间才能理解,所以如果你第一次没有理解,不要担心。

就像你可以用名字、姓氏或两者来指代人一样,派生类的对象可以使用以下任何类型的引用变量来引用:

  • 其自身类型—— 一个HRExecutive类的对象可以使用类型为HRExecutive的对象引用变量来引用。

  • 其超类—— 如果类 HRExecutive 继承了类 Employee,则可以使用类型为 Employee 的变量来引用 HRExecutive 类的对象。如果类 Employee 继承了类 Person,则也可以使用类型为 Person 的变量来引用 HRExecutive 类的对象。

  • 实现接口— 如果类 HRExecutive 实现了接口 Interviewer,则可以使用类型为 Interviewer 的变量来引用 HRExecutive 类的对象。

然而,当您尝试使用其自身类型、基类或实现接口的引用变量来访问对象时,会有所不同。让我们从使用其自身类型的变量来访问对象开始。

6.3.1. 使用派生类变量访问其自身对象

让我们从继承自类 Employee 并实现接口 Interviewer 的类 HRExecutive 的代码开始,如下所示:

下面有一些代码演示了可以使用类型为 HRExecutive 的变量来引用 HRExecutive 类的对象:

您可以使用类型为 HRExecutive 的变量 hr 访问在类 Employee、类 HRExecutive 和接口 Interviewer 中定义的字段和方法,如下所示:

当您使用其自身类型来访问 HRExecutive 类的对象时,您可以访问在其基类和接口中定义的所有变量和方法——即类 Employee 和接口 Interviewer。如果将引用变量的类型更改为在下一节中定义的类 Employee,您能做同样的事情吗?

6.3.2. 使用超类变量访问派生类对象

让我们使用类型为 Employee 的引用变量来访问类型为 HRExecutive 的对象,如下所示:

现在,让我们看看在访问类 Employee、类 HRExecutive 或接口 Interviewer 的成员时,更改引用变量的类型是否会有任何不同。下面的代码能否成功编译?

代码在 处无法编译,因为变量 emp 的类型被定义为 Employee。想象一下:变量 emp 只能看到 Employee 对象。因此,它只能访问在类 Employee 中定义的变量和方法,如图 6.18 所示。

图 6.18. 类型为 Employee 的变量只能看到在类 Employee 中定义的成员。

6.3.3. 使用实现接口的变量访问派生类对象

这里还有一个有趣的等式:当您将引用变量的类型更改为接口 Interviewer 时会发生什么?由于类 HRExecutive 实现了 Interviewer,因此类型为 Interviewer 的变量也可以用来引用 HRExecutive 类的对象。请看以下代码:

class Office {
    public static void main(String args[]) {
        Interviewer interviewer = new HRExecutive();
    }
}

现在尝试使用引用变量interviewer(它指向HRExecutive类的对象)来访问相同的变量和方法集:

代码在处无法编译,因为变量interviewer的类型被定义为Interviewer。想象一下:变量interviewer只能访问接口Interviewer中定义的方法,如图 6.19 所示。

图 6.19. 类型为Interviewer的变量只能看到在Interviewer接口中定义的成员。

6.3.4. 需要通过基类或实现接口的变量来访问对象

你可能想知道,为什么你需要基类或实现接口的引用变量来访问派生类的对象,如果变量不能访问派生类对象可用的所有成员。简单的答案是,你可能对派生类的所有成员不感兴趣。

感到困惑?比较以下情况。当你报名参加飞行课程时,你是否关心教练是否会做意大利菜或会游泳?不!你不会关心与飞行无关的特征和行为。这里还有一个例子。在办公室派对上,所有员工都受欢迎,无论他们是程序员、HRExecutive 还是 Manager,如图 6.20 所示。

图 6.20. 所有类型的员工都可以参加办公室派对。

当你使用类型为Interviewer的引用变量访问HRExecutive类的对象时,相同的逻辑也适用。当你这样做时,你只关心与HRExecutive作为Interviewer的能力相关的行为。

这种安排还使得能够创建一个数组(或列表),该数组引用了按公共基类或接口分组的不同类型的对象。以下代码段定义了一个类型为Interviewer的数组,并在其中存储了HRExecutiveManager类的对象:

HRExecutive扩展了类Employee并实现了接口Interviewer。因此,你可以将HRExecutive对象赋值给以下任何类型的变量:

  • HRExecutive

  • Employee

  • Interviewer

  • Object

请注意,这些赋值的逆操作将无法通过编译。首先,你不能使用派生类的引用变量来引用基类的对象。因为派生类的所有成员都不能通过基类的对象访问,这是不允许的。以下语句将无法编译:

因为不能创建接口的对象,所以以下代码行也将无法编译:

现在是时候尝试在你的下一个故事转折练习中添加之前定义的相关类的对象——EmployeeManagerHRExecutive——到数组中(答案在附录中)。

故事转折 6.2

给定以下 EmployeeManagerHRExecutive 类以及 Interviewer 接口的定义,为类 TwistInTale2 选择正确的选项:

class Employee {}
interface Interviewer {}
class Manager extends Employee implements Interviewer {}
class HRExecutive extends Employee implements Interviewer {}

class TwistInTale2 {
    public static void main (String args[]) {
        Interviewer[] interviewer = new Interviewer[] {
                new Manager(),           // Line 1
                new Employee(),          // Line 2
                new HRExecutive(),       // Line 3
                new Interviewer()        // Line 4
            };
    }
}
  1. Manager 的对象可以被添加到接口 Interviewer 的数组中。第 1 行的代码将成功编译。

  2. Employee 的对象可以被添加到接口 Interviewer 的数组中。第 2 行的代码将成功编译。

  3. HRExecutive 的对象可以被添加到接口 Interviewer 的数组中。第 3 行的代码将成功编译。

  4. 接口 Interviewer 的对象可以被添加到接口 Interviewer 的数组中。第 4 行的代码将成功编译。

考试技巧

你可能会在考试中看到多个问题,尝试将基类对象分配给派生类引用变量。请注意,派生类可以使用超类引用变量来引用。反之则不允许,并且无法编译。

在本节中,你了解到基类或接口的变量无法访问它们所引用的对象的所有成员。不用担心;这可以通过将基类或接口的引用变量转换为它们所引用的对象的确切类型来解决,正如下一节所讨论的。

6.4. 类型转换

[7.3] 确定何时需要进行类型转换

类型转换 是强制使变量表现得像另一个类型的变量的过程。如果一个类与另一个类或接口共享 IS-A 或继承关系,它们的变量可以转换为彼此的类型。

在 第 6.3 节 中,你了解到如果你通过类型为 Interviewer(实现接口)或 Employee(基类)的变量来引用类 HRExecutive(派生类),则无法访问该类的所有成员。在本节中,你将学习如何将类型为 Interviewer 的变量转换为类 HRExecutive 中定义的变量,以及为什么需要这样做。

6.4.1. 如何将变量转换为另一种类型

我们将从接口 Interviewer 和类 HRExecutive 以及 Manager 的定义开始:

class Employee {}
interface Interviewer {
    public void conductInterview();
}
class HRExecutive extends Employee implements Interviewer {
    String[] specialization;
    public void conductInterview() {
        System.out.println("HRExecutive - conducting interview");
    }
}
class Manager implements Interviewer{
    int teamSize;
    public void conductInterview() {
        System.out.println("Manager - conducting interview");
    }
}

创建一个类型为 Interviewer 的变量,并将其分配给类型为 HRExecutive 的对象(如图 6.21 所示):

Interviewer interviewer = new HRExecutive();
图 6.21. 接口 Interviewer 的引用变量指向类 HRExecutive 的对象

尝试使用之前的变量访问在类 HRExecutive 中定义的变量 specialization

上一行代码将无法编译。编译器知道变量interviewer的类型是Interviewer,并且接口Interviewer没有定义名为specialization的任何变量(如图 6.22 所示)。

图 6.22。如果你尝试使用Interviewer接口的变量来访问在HRExecutive类中定义的变量specialization,Java 编译器将不会编译代码。

另一方面,JRE 知道变量interviewer所引用的对象是HRExecutive类型,因此你可以使用类型转换来绕过 Java 编译器,访问所引用对象的成员,如下所示(也请参见图 6.23):

((HRExecutive)interviewer).specialization = new String[] {"Staffing"};
图 6.23。可以使用类型转换通过Interviewer接口的变量来访问在HRExecutive类中定义的变量specialization

在前面的示例代码中,(HRExecutive)放置在变量名interviewer之前,以将其转换为HRExecutive。一对括号包围了HRExecutive,这使 Java 知道你确信所引用的对象是HRExecutive类的对象。类型转换是另一种告诉 Java 的方法,“看,我知道实际所引用的对象是HRExecutive,尽管我正在使用类型为Interviewer的引用变量。”

为了绕过 Java 运算符优先级规则,需要将整个(HRExecutive)interviewer标记包围在括号中,根据这些规则,类型转换“运算符”(括号)的优先级低于点“运算符”(用于访问对象字段或调用方法)。

6.4.2. 类型转换的需要

在第 6.3.4 节中,我讨论了使用继承类或实现接口的引用变量来引用派生类对象的需要。我还用一个报名飞行课程的例子来说明,你并不关心教练是否能做意大利菜或是否会游泳。你并不关心与飞行无关的特征和行为。

但考虑一下这种情况,你确实关心你的教练的游泳技巧。想象一下,当你正在上飞行课程时,你的朋友询问你的飞行教练是否也开设游泳课程,如果是的话,你的朋友是否可以报名。在这种情况下,就有必要了解你的飞行教练的游泳技巧。

让我们将这种情况应用到 Java 中。如果你使用任何实现接口或基类的引用变量来访问对象,你将无法访问该对象的所有成员。但是,当出现需要(如前一段所述)时,你可能会选择使用基类型或实现接口的引用变量来访问一些派生类的成员,这些成员不是明确可用的。这就是类型转换发挥作用的地方!

是时候在代码中看到这一点了。以下是一个展示需要类型转换的示例。一个应用程序维护一个面试官列表,根据面试官的类型(HRExecutiveManager),它执行不同的操作集。如果面试官是 Manager,则只有在 ManagerteamSize 值大于 10 时,代码才会调用 conductInterview。以下是实现此逻辑的代码:

上述代码展示了类型转换的最佳实践,即 interviewer instanceof Manager。如果您省略了这个测试,代码可能会抛出 ClassCastException(在第 7.5.2 节中详细说明)。

6.5. 使用 this 和 super 访问对象和构造函数

[7.4] 使用 super 和 this 访问对象和构造函数

在本节中,您将使用 thissuper 关键字来访问对象和构造函数。thissuper隐式对象引用。这些变量由 JVM 为其内存中的每个对象定义和初始化。

让我们检查每个这些引用变量的能力和用法。

6.5.1. 对象引用:this

this 引用始终指向对象的自身实例。任何对象都可以使用 this 引用来引用其自身的实例。想想看单词 memyselfI:使用这些单词的人总是指自己,如图 6.24 所示。

图 6.24. 关键字 this 可以与单词 memyselfI 相比。

使用 this 访问变量和方法

您可以使用关键字 this 来引用类可访问的所有方法和变量。例如,以下是 Employee 类的修改后定义:

class Employee {
    String name;
}

变量 name 可以在扩展了 Employee 类的 Programmer 类中访问,如下所示:

class Programmer extends Employee {
    void accessEmployeeVariables() {
         name = "Programmer";
    }
}

因为在 Programmer 类中存在 Employee 类的成员,所以变量 name 可以被 Programmer 类的对象访问。变量 name 也可以在 Programmer 类中按如下方式访问:

class Programmer extends Employee {
    void accessEmployeeVariables() {
         this.name = "Programmer";
    }
}

只有当方法块中的代码需要区分实例变量和它的局部变量或方法参数时,才需要 this 引用。但一些开发者即使在不需要时也会在他们的代码中使用关键字 this。有些人使用 this 作为区分实例变量和局部变量或方法参数的手段。

图 6.25 展示了 Employee 类的构造函数,它使用引用变量 this 来区分具有相同名称的局部变量 name 和实例变量。

图 6.25. 使用关键字 this 区分方法参数和实例变量

在前面的例子中,类Employee定义了一个实例变量nameEmployee类构造函数还定义了一个方法参数name,这实际上是在方法块作用域内定义的一个局部变量。因此,在先前定义的Employee构造函数的作用域内,存在名称冲突,局部变量将具有优先权(在第 3.1 节中介绍)。在Employee类构造函数块的作用域内使用name将隐式地引用该方法的参数,而不是实例变量。为了从Employee类构造函数的作用域内引用实例变量name,你必须使用this引用。

使用此方法访问构造函数

你也可以通过使用关键字this从另一个构造函数引用一个构造函数。以下是一个例子,其中类Employee定义了两个构造函数,第二个构造函数调用了第一个:

要调用默认构造函数(不接受任何方法参数的构造函数),调用this()。以下是一个例子:

如果存在,从一个构造函数调用另一个构造函数必须在调用构造函数的第一行代码上完成。

考试技巧

this指的是使用它的类的实例。this可以用来访问派生类中基类的继承成员。

在接口中使用关键字 this

在 Java 8 中,你可以在接口的default方法中使用关键字this来访问其常量和其他默认和abstract方法。在以下示例中,接口Interviewer定义了一个默认方法submitInterviewStatus。此方法使用this来访问自身及其常量或方法:

interface Interviewer {
    int MIN_SAL = 9999;
    default void submitInterviewStatus() {
        System.out.println(this);
        System.out.println(this.MIN_SAL);
        System.out.println(this.print());
    }
    String print();
}
class Manager implements Interviewer {
    public String print() {
        return("I am " + this);
    }
}
class Foo {
    public static void main(String rags[]) {
        Interviewer m = new Manager();
        m.submitInterviewStatus();
    }
}

你可能会看到前面代码的类似输出:

Manager@19e0bfd
9999
I am Manager@19e0bfd
考试技巧

在 Java 8 中,你可以在默认方法中使用关键字this来访问接口的方法和常量。

你不能使用this关键字来访问接口的static方法。

6.5.2. 对象引用:super

在上一节中,我讨论了this如何指向对象实例本身。同样,super也是一个对象引用,但super指向类的直接父类或基类。想想看,“我的父母”,“我的基类”:使用这些术语的人总是指他们的直接父母或基类,如图 6.26 所示。

图 6.26。当一个类提到super时,它指的是它的直接父类或基类。

使用 super 访问基类的变量和方法

当这些名称之间发生冲突时,可以使用变量引用super来访问基类中的变量或方法。这种情况通常发生在派生类定义了与基类具有相同名称的变量和方法时。

这里是一个例子:

前面代码的输出如下:

Employee
Programmer

同样,你可以使用引用变量 super 来访问在基类或父类中定义的同名方法。

使用 super 访问基类构造函数

引用变量 super 也可以在派生类中用来引用基类的构造函数。

这里有一个例子,其中基类 Employee 定义了一个构造函数,它为其变量分配默认值。其派生类在其自己的构造函数中调用基类构造函数。

代码在 处通过传递引用变量 nameaddress 调用超类构造函数,它自己接受这些变量。

考试技巧

如果存在,派生类构造函数中对超类构造函数的调用必须是派生类构造函数中的第一条语句。否则,编译器会自动插入对 super();(无参数构造函数)的调用。

在静态方法中使用 super 和 this

关键字 superthis 是隐式对象引用。因为 static 方法属于一个类,而不是属于类的对象,所以你无法在 static 方法中使用 thissuper。尝试这样做的代码将无法编译:

是时候尝试下一个故事转折练习了,使用 thissuper 关键字(答案见附录)。

故事转折 6.3

让我们修改 EmployeeProgrammer 类的定义如下。TwistInTale3 类的输出是什么?

class Employee {
    String name = "Emp";
    String address = "EmpAddress";
}
class Programmer extends Employee{
    String name = "Prog";
    void printValues() {
        System.out.print(this.name + ":");
        System.out.print(this.address + ":");
        System.out.print(super.name + ":");
        System.out.print(super.address);
    }
}
class TwistInTale3 {
    public static void main(String args[]) {
        new Programmer().printValues();
    }
}
  1. Prog:null:Emp:EmpAddress

  2. Prog:EmpAddress:Emp:EmpAddress

  3. Prog::Emp:EmpAddress

  4. 编译错误

同样,你也不能在接口中定义的 static 方法中使用关键字 this

现在让我们转向一个非常重要的编程概念:多态。在下一节中,你将使用抽象类和接口来实现它。

6.6. 多态

[7.5] 使用抽象类和接口

[7.2] 开发演示多态使用的代码;包括重写和对象类型与引用类型

“多态”这个词的字面意思是“多种形式”。在本章的开头,我使用了一个实际例子来解释多态的含义;同样的动作对不同生物可能具有不同的意义。动作 苍蝇狮子 来说有不同的意义。一只 苍蝇 可能吃 花蜜,而一只 狮子 可能吃 羚羊。生物对同一动作以独特的方式做出反应可以与 Java 中的多态相提并论。

对于考试,你需要知道 Java 中的多态是什么,为什么需要它,以及如何在代码中实现它。

6.6.1. 类的多态

当一个类继承另一个类,并且基类和派生类都定义了具有相同方法签名(相同的方法名和方法参数)的方法时,就会出现类多态的情况。正如前文所述,一个对象也可以使用其基类的引用变量来引用。在这种情况下,根据执行方法的对象类型,Java 运行时会执行基类或派生类中定义的方法。

让我们通过EmployeeProgrammerManager这三个类来考虑多态性,其中ProgrammerManager类继承自Employee类。图 6.27 展示了这些类之间的关系。

图 6.27. EmployeeProgrammerManager类之间的关系

我们首先从Employee类开始,它并不确定为了开始一个项目的工作必须做什么(执行startProjectWork方法)。因此,startProjectWork方法被定义为abstract方法,而Employee类被定义为abstract类,如下所示:

Programmer类继承自Employee类,这意味着它能够访问在Employee中定义的reachOffice方法。Programmer类还必须实现从Employee继承来的abstract方法startProjectWork。你认为程序员通常会怎样开始一个编程项目的工作?很可能是,程序员会定义类并对它们进行单元测试。这种行为包含在Programmer类的定义中,该类实现了start-ProjectWork方法,如下所示:

class Programmer extends Employee {
    public void startProjectWork() {
        defineClasses();
        unitTestCode();
    }
    private void defineClasses() { System.out.println("define classes"); }
    private void unitTestCode() { System.out.println("unit test code"); }
}

我们很幸运还有另一种特殊的员工类型,即经理,他知道如何开始一个项目的工作。你认为经理通常会怎样开始一个编程项目的工作?很可能是,经理会与客户会面,定义项目进度,并分配工作给团队成员。以下是扩展Employee类并实现startProjectWork方法的Manager类的定义:

class Manager extends Employee {
    public void startProjectWork() {
        meetingWithCustomer();
        defineProjectSchedule();
        assignRespToTeam();
    }
    private void meetingWithCustomer() {
        System.out.println("meet Customer");
    }
    private void defineProjectSchedule() {
        System.out.println("Project Schedule");
    }
    private void assignRespToTeam() {
        System.out.println("team work starts");
    }
}

让我们看看这个方法在不同类型的员工中是如何表现的。以下是相关的代码:

这是代码的输出(为了清晰,添加了空白行):

reached office - Gurgaon, India
reached office - Gurgaon, India

define classes
unit test code

meet Customer
Project Schedule
team work starts

中的代码创建了一个Programmer类的对象,并将其分配给一个Employee类型的变量。创建了一个Manager类的对象,并将其分配给一个Employee类型的变量。到目前为止,一切顺利!

现在是复杂的部分。执行了reachOffice方法。因为这个方法只在Employee类中定义,所以没有混淆,执行了相同的方法,并打印了以下内容:

reached office - Gurgaon, India
reached office - Gurgaon, India

num-4.jpg处的代码执行emp1.startProjectWork()并调用在Programmer类中定义的startProjectWork方法,因为emp1引用的是Programmer类的一个对象。以下是这个方法调用的输出:

define classes
unit test code

num-5.jpg处的代码执行emp2.startProjectWork()并调用在Manager类中定义的startProjectWork方法,因为emp2引用的是Manager类的一个对象。以下是这个方法调用的输出:

meet Customer
Project Schedule
team work starts

图 6.28 展示了这段代码。

图 6.28。对象知道自己的类型,并执行它们自己类中定义的覆盖方法,即使使用基类变量来引用它们。

06fig28_alt.jpg

如本节开头所述,多态的有用之处在于对象能够在接收到相同操作时以自己的特定方式行为。在前面的例子中,使用类型为Employee的引用变量(emp1emp2)来存储ProgrammerManager类的对象。当在引用变量(emp1emp2)上调用相同的操作——即方法调用startProjectWork——时,每个方法调用都会导致执行相应类中定义的方法。

多态方法也称为覆盖方法

快速查看以下类EmployeeProgrammerManager(只显示相关代码)中定义的startProjectWork方法:

437fig01_alt.jpg

注意,所有这些类中的startProjectWork方法名称相同。它们接受相同数量的方法参数,并在三个类EmployeeProgrammerManager中定义相同的返回类型:这是指定覆盖方法的契约。未能使用相同的方法名称、相同的参数列表或相同的返回类型不会将方法标记为覆盖方法。

记住覆盖方法的规则

这里是定义覆盖方法时需要注意的规则集:

  • 覆盖方法是由具有继承关系的类和接口定义的。

  • 基类中覆盖方法的名称和子类中覆盖方法的名称必须相同。

  • 在基类中传递给覆盖方法的参数列表必须与在子类中传递给覆盖方法的参数列表相同。

  • 子类中覆盖方法的返回类型可以与基类中覆盖方法的返回类型相同或为其子类。当覆盖方法返回覆盖方法的返回类型的子类时,它被称为协变返回类型

  • 在基类中定义的覆盖方法可以是abstract方法或非abstract方法。

  • 派生类只能覆盖非final方法。

  • 覆盖方法的可访问修饰符可以与被覆盖的方法相同或更少限制,但不能更严格。

多态方法是否总是必须是抽象的?

不,多态方法不总是必须是 abstract。你可以将 Employee 类定义为具体类,将 startProjectWork 方法定义为非 abstract 方法,仍然可以得到相同的结果(加粗部分):

class Employee {
    public void reachOffice() {
        System.out.println("reached office - Gurgaon, India");
    }
    public void startProjectWork() {
        System.out.println("procure hardware");
        System.out.println("install software");
    }
}

由于其他类的定义(ProgrammerManagerPolymorphismWithClasses)没有变化,我没有在这里列出它们。如果你创建了一个 Employee 类的对象(不是其任何派生类的对象),你可以如下执行 startProjectWork 方法:

图片

考试技巧

要使用类实现多态,你可以在基类中定义 abstract 或非 abstract 方法,并在派生类中重写它们。

多态能否与重载方法一起工作?

不,多态只与重写方法一起工作。重写方法具有相同数量和类型的参数,而重载方法定义了一个具有不同数量或类型的参数的方法参数列表。

重载方法只有相同的名称;JRE 将它们视为不同的方法。在重写方法的情况下,JRE 根据被调用对象的精确类型在运行时决定调用哪个方法。

是时候进行下一个故事转折练习了。像往常一样,你可以在附录中找到答案。

故事转折 6.4

给定以下 EmployeeProgrammer 类的定义,以下哪个选项在 //INSERT CODE HERE// 处插入将定义 run 方法为多态方法?

class Employee {
    //INSERT CODE HERE// {
        System.out.println("Emp-run");
        return null;
    }
}
class Programmer extends Employee{
    String run() {
        System.out.println("Programmer-run");
        return null;
    }
}
class TwistInTale4 {
    public static void main(String args[]) {
        new Programmer().run();
    }
}
  1. String run()

  2. void run(int meters)

  3. void run()

  4. int run(String race)

6.6.2. 编译时和运行时变量和方法绑定

你可以使用基类的引用变量来引用派生类的对象。但是,Java 访问这些对象的变量和方法的方式有一个主要区别。在继承中,实例变量在编译时绑定,方法在运行时绑定。

注意

绑定 指的是解析变量或方法,这些变量或方法将用于引用变量。

检查以下代码:

图片

上述代码的输出如下:

Employee
Employee
Employee
Programmer

让我们逐步查看代码中发生了什么:

  • 图片 创建了一个 Employee 类的对象,该对象通过其自身类型的变量引用——Employee

  • 图片 创建了一个 Programmer 类的对象,该对象通过其基类型的变量引用——Employee

  • 图片 访问了在类 Employee 中定义的变量 name 并打印了 Employee

  • 图片 也打印了 Employee。变量 programmer 的类型是 Employee。因为变量在编译时绑定,所以变量 emp 所引用的对象的类型并不重要。programmer.name 将访问在类 Employee 中定义的变量 name

  • 打印 Employee。因为引用变量 emp 的类型和它引用的对象的类型相同(Employee),所以在方法调用上没有混淆。

  • 打印 Programmer。尽管使用 Employee 类型的引用调用了 printName 方法,但 JRE 知道该方法是在 Programmer 对象上调用的,因此执行了 Programmer 类中重写的 printName 方法。

考试技巧

在考试中要注意使用基类变量来引用派生类对象,然后访问引用对象变量和方法的情况。记住,变量在编译时绑定,而方法在运行时绑定。

6.6.3. 接口的多态

多态也可以通过接口实现。与类多态不同,接口多态需要一个类实现一个接口。接口多态涉及实现接口的 abstract 或默认方法。接口还可以定义静态方法,但静态方法永远不会参与多态。

抽象方法的多态

让我们从例子开始。这里有一个名为 MobileAppExpert 的接口,它定义了一个 abstract 方法 deliverMobileApp

interface MobileAppExpert {
    void deliverMobileApp();
}

这里是实现了该接口和 deliverMobileApp 方法的 ProgrammerManager 类的简化版本:

class Employee {}
class Programmer extends Employee implements MobileAppExpert {
    public void deliverMobileApp() {
        System.out.println("testing complete on real device");
    }
}
class Manager extends Employee implements MobileAppExpert {
    public void deliverMobileApp() {
        System.out.println("QA complete");
        System.out.println("code delivered with release notes");
    }
}

两个类和接口之间的关系在 图 6.29 中显示。

图 6.29. EmployeeProgrammerManager 类以及 MobileAppExpert 接口之间的关系

在现实世界中,移动应用程序的交付对程序员和管理员有不同的含义。对于程序员来说,移动应用程序的交付可能需要完成在真实移动设备上的测试。但对于管理员来说,移动应用程序的交付可能意味着完成质量保证过程,并将代码连同任何发布说明一起移交给客户。总之,同一个消息 deliverMobileApp 对程序员和管理员来说会导致执行不同的步骤集。

这里有一个名为 PolymorphismWithInterfaces 的类,它创建了 ProgrammerManager 类的对象,并调用了 deliverMobileApp 方法:

上述代码的输出如下:

testing complete on real device
QA complete
code delivered with release notes

,变量的类型是 MobileAppExpert。因为 ManagerProgrammer 类实现了 MobileAppExpert 接口,所以也可以使用 MobileAppExpert 类型的引用变量来存储 ProgrammerManager 类的对象。

因为这两个类也扩展了 Employee 类,所以你可以使用 Employee 类型的变量来存储 ProgrammerManager 类的对象。但在这个情况下,你将无法调用 deliverMobileApp 方法,因为它对 Employee 类不可见。检查以下代码:

图片

让我们看看如果将 Employee 类修改为实现 MobileAppExpert 接口会发生什么,如下所示:

class Employee implements MobileAppExpert {
    // code
}
interface MobileAppExpert {
    // code
}

现在,类 ProgrammerManager 只需扩展 Employee 类。它们不再需要实现 MobileAppExpert 接口,因为它们的基类 Employee 实现了它:

class Programmer extends Employee {
    // code
}
class Manager extends Employee {
    // code
}

通过修改后的代码,展示了类 EmployeeManagerProgrammer 以及接口 MobileAppExpert 之间新的关系,如图 6.30 所示。

图 6.30. EmployeeManagerProgrammer 类以及接口 MobileAppExpert 之间的修改后的关系

图片

让我们尝试使用 Employee 类型的引用变量来访问 deliverMobileApp 方法,如下所示:

图片

图 6.31 展示了变量 expert1 可以访问的内容。

图 6.31. 变量 expert1 可以访问的内容

图片

考试技巧

当心那些看似参与多态的过度载方法——过度载方法并不参与多态。只有重写的方法——具有相同方法签名的那些方法——才参与多态。

默认方法的多态

当一个类实现了一个定义默认方法的接口时,该类可能或可能不会重写默认方法。在以下示例中,类 Manager 重写了接口 Interviewer 中定义的默认方法 submitInterviewStatus

interface Interviewer {
    default Object submitInterviewStatus() {
        System.out.println("Interviewer:Accept");
        return null;
    }
}
class Manager implements Interviewer {
    public String submitInterviewStatus() {
        System.out.println("Manager:Accept");
        return null;
    }
}

class Project {
    public static void main(String args[]) {
        Interviewer interviewer = new Manager();
        interviewer.submitInterviewStatus();

        Manager mgr = new Manager();
        mgr.submitInterviewStatus();
    }
}

这是上述代码的输出:

Manager:Accept
Manager:Accept

在前面的代码中,尽管 Project 类使用接口 InterviewerManager 类的引用变量来引用 Manager 实例,但 submitInterviewStatus() 的调用被委派给了在 Manager 类中定义的重写方法。

这里有一个有趣的情况。想象有两个接口,BaseInterface1BaseInterface2,它们定义了具有相同名称的默认方法,getName()。这两个接口被另一个接口 MyInterface 扩展,该接口重写了 getName 方法。现在,想象一个类 MyClass 实现了这三个接口。当你调用 MyClass 实例上的 getName() 时,输出是什么?它能否编译?

interface BaseInterface1 {
    default void getName() { System.out.println("Base 1"); }
}
interface BaseInterface2 {
    default void getName() { System.out.println("Base 2"); }
}
interface MyInterface extends BaseInterface1, BaseInterface2 {
    default void getName() { System.out.println("Just me"); }
}
class MyClass implements BaseInterface1, BaseInterface2, MyInterface {
    public static void main(String ar[]) {
        new MyClass().getName();
    }
}

上述代码编译成功并输出 Just me。在类 MyClass 的声明中使用接口名称 BaseInterface1BaseInterface2 是多余的(重复的),因为 MyInterface 已经扩展了 BaseInterface1Base-Interface2。所以 MyClass 只继承了一个默认方法 getName 的实现,而不是三个。它继承的是在接口 MyInterface 中定义的 getName()

6.7. 简单的 Lambda 表达式

[9.5] 编写一个简单的 Lambda 表达式,它消费一个 Lambda 断言表达式

本考试包括使用简单的 Lambda 表达式,以便你能够开始使用 Java 中的 函数式编程风格。函数式编程使你能够编写声明式代码。它让你定义 要做什么,而不是专注于 如何做。使用函数式编程,你可以将代码作为参数传递给你的方法。让我们通过比较向方法传递变量或文字值与向它们传递代码来熟悉它。

6.7.1. 比较向方法传递值与向方法传递代码

假设你需要编写方法来打印一系列数字的值,比如 1 到 10,10 到 20,等等,而不需要将参数传递给方法。下面是你可能编写的代码:

class NoMethodParameters{
    void print1To10() {
        for (int i = 1; i <= 10; i++)
            System.out.println(i);
    }
    void print10To20() {
        for (int i = 10; i <= 20; i++)
            System.out.println(i);
    }
    void print1To99() {
        for (int i = 1; i <= 99; i++)
            System.out.println(i);
    }
}

因为你知道如何为方法定义方法参数,你肯定会认为定义前面代码中的方法是不理智的。所以这里有一个接受方法参数的方法:

class WithMethodParameters {
    void printNumbers(int start, int end) {
        for (int i = start; i <= end; i++)
            System.out.println(i);
    }
}

下面是如何调用前面代码中定义的打印整数的方法的示例:

NoMethodParameters noParameters = new NoMethodParameters();
noParameters.print10To20();
noParameters.print1To99();

WithMethodParameters withParameters = new WithMethodParameters();

withParameters.printNumbers(10, 20);
withParameters.printNumbers(1, 99);
withParameters.printNumbers(100, 200);
withParameters.printNumbers(500, 1000);

注意你可以只定义一个方法,printNumbers,并用多个值调用它。让我们应用相同的逻辑来定义一个方法,传递代码,这样我们就不需要它的多个实现了。

在使用 Lambda 之前,让我们用一个不使用它们的例子来工作,以突出其优点。以下示例定义了一个类 Emp(包含一些实例变量)。它还定义了一个接口 Validate,该接口定义了一个 abstract 方法 check。它的目的是检查 Emp 实例的状态并返回一个 boolean 值:

interface Validate {
    boolean check(Emp emp);
}
class Emp {
    String name;
    int performanceRating;
    double salary;
    Emp(String nm, int rating, double sal) {
        name = nm;
        performanceRating = rating;
        salary = sal;
    }
    public String getName() { return name; }
    public int getPerformanceRating() { return performanceRating; }
    public double getSalary() { return salary; }
    public String toString() {
        return name + ":" + performanceRating + ":" + salary;
    }
}

要使用接口 Validate(而不使用 Lambda),你可以定义一个实现它的类或定义匿名类。因为匿名类不在这个考试中,我将定义一个实现接口 Validate 的类。在以下代码中,类 ValidatePerformanceRating 检查一个 Emp 实例,如果 Emp 实例的 performanceRating 大于或等于 5,则返回 true

class ValidatePerformanceRating implements Validate{
    public boolean check(Emp emp) {
        return (emp.getPerformanceRating() >= 5);
    }
}

如果你想要检查 Emp 实例的另一个属性,比如 name,你需要另一个类:

class ValidateName implements Validate{
    public boolean check(Emp emp) {
        return (emp.getName.startsWith("P"));
    }
}

将前面的类——ValidateNameValidatePerformanceRating——与类 NoMethodParameters 中的多个 printXXX 方法进行比较。注意在 ValidateNameValidatePerformanceRating 类的 check 方法中,只是 boolean 条件在变化。下面是如何在方法中使用 ValidateNameValidatePerformanceRating 实例的示例,比如 filter 方法:

class Test {
    public static void main(String args[]) {
        Emp e1 = new Emp("Shreya", 5, 9999.00);
        Emp e2 = new Emp("Paul", 4, 1234.00);
        Emp e3 = new Emp("Harry", 5, 8769.00);
        Emp e4 = new Emp("Selvan", 1, 2769.00);

        ArrayList<Emp> empArrList = new ArrayList<>();
        empArrList.add(e1);
        empArrList.add(e2);
        empArrList.add(e3);
        empArrList.add(e4);

        filter(empArrList, new ValidatePerformanceRating());
    }
    static void filter(ArrayList<Emp> list, Validate rule) {
        for (Emp e : list) {
            if (rule.check(e)) {
                System.out.println(e);
            }
        }
    }
}

在前面的代码中,filter 方法接受一个 EmpArrayList 并输出那些通过使用接口 Validatecheck 方法检查 Emp 实例时返回 true 的实例。

如前所述,你需要创建多个类(实现接口Validate)以使用不同的有效性规则。除了大部分是重复的,它还非常冗长。Lambdas 来拯救!让我们删除ValidateNameValidatePerformanceRating类的定义。为了定义验证条件,我们将使用 lambda(修改后的代码用粗体表示):

448fig01_alt.jpg

在前面的代码中,对接受类型为Validate(一个接口)的方法参数的filter方法(没有变化)。在![num-1.jpg]处的代码定义了一个 lambda 表达式。它定义了要传递给filter方法的代码。将 lambda 表达式映射到Validate中的check方法签名。check方法接受一个方法参数,lambda 表达式也是如此,即(e)check方法返回一个boolean值,lambda 表达式中的表达式e.getPerformanceRating() >= 5也是如此。

注意

Lambdas 仅与功能接口一起工作——定义了恰好一个抽象方法的接口。

让我们在下一节深入探讨 lambda 表达式的细节。

6.7.2. Lambda 表达式的语法

让我们回顾一下之前表达式中使用的 lambda 表达式:

Validate validatePerfor = e -> e.getPerformanceRating() >= 5;

上述代码仅包含 lambda 表达式的必要部分,如图 6.32 所示。

图 6.32. 一个 Lambda 表达式及其必要部分

06fig32.jpg

每个 lambda 表达式都有多个可选和必要部分:

  • 参数类型(可选)

  • 参数名称(必要)

  • 箭头(必要)

  • 大括号(可选)

  • 关键字return(可选)

  • Lambda 体(必要)

以下是对前面 lambda 表达式的有效变体(修改用粗体表示):

Validate validate = (e) -> e.getPerformanceRating() >= 5;
Validate validate = (Emp e) -> e.getPerformanceRating() >= 5;
Validate validate = (e) -> { return (e.getPerformanceRating() >= 5); };

在考试中,你需要识别无效的 lambda 表达式。lambda 表达式的返回值必须与接口中唯一的抽象方法的返回值匹配或兼容。接口Validate中的check方法声明其返回类型为boolean。因此以下将是无效的:

450fig01_alt.jpg

如果你尝试向 lambda 表达式传递错误数量的方法参数,代码将无法编译:

450fig02_alt.jpg

Java 8 为您的方便添加了多个功能接口。本考试仅涵盖其中之一——接口Predicate,将在下一节讨论。

6.7.3. 接口谓词

Predicate是一个功能接口。以下是该接口的部分定义:

public interface Predicate<T> {
    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    // rest of the code
}

在前面的代码中,类声明包括<T>,这表明Predicate-是一个泛型接口,它不受特定类型的限制。它可以与多种类型一起使用。泛型在 OCP Java SE 8 程序员 II 考试中详细讨论。

要在代码中使用 Predicate,你的方法必须接受类型为 Predicate 的参数,并且必须使用其公共方法 test 来评估一个参数。让我们修改 6.7.1 节 中使用的示例,用 Predicate 代替 Validate(加粗部分):

class Test {
    public static void main(String args[]) {
        Emp e1 = new Emp("Shreya", 5, 9999.00);
        Emp e2 = new Emp("Paul", 4, 1234.00);
        Emp e3 = new Emp("Harry", 5, 8769.00);
        Emp e4 = new Emp("Selvan", 1, 2769.00);

        ArrayList<Emp> empArrList = new ArrayList<>();
        empArrList.add(e1);
        empArrList.add(e2);
        empArrList.add(e3);
        empArrList.add(e4);

        Predicate<Emp> predicate = e -> e.getPerformanceRating() >= 5;
        filter(empArrList, predicate);
    }
    static void filter(ArrayList<Emp> list, Predicate<Emp> rule) {
        for (Emp e : list) {
            if (rule.test(e)) {
                System.out.println(e);
            }
        }
    }
}

Java 8 也修改了许多现有的 API 方法,这些方法与函数式接口如 Predicate 一起工作。例如,ArrayList 类定义了 removeIf 方法,该方法接受类型为 Predicate 的方法参数。以下示例展示了 removeIf 的用法:

Emp e1 = new Emp("Shreya", 5, 9999.00);
Emp e2 = new Emp("Paul", 4, 1234.00);
Emp e3 = new Emp("Harry", 5, 8769.00);
Emp e4 = new Emp("Selvan", 1, 2769.00);

ArrayList<Emp> empArrList = new ArrayList<>();
empArrList.add(e1);
empArrList.add(e2);
empArrList.add(e3);
empArrList.add(e4);

for (Emp e : empArrList)
    System.out.println(e);

System.out.println("After deletion..");

empArrList.removeIf(e -> e.getName().startsWith("S"));

for (Emp e : empArrList)
    System.out.println(e);

以下是前述代码的输出:

Shreya:5:9999.0
Paul:4:1234.0
Harry:5:8769.0
Selvan:1:2769.0
After deletion..
Paul:4:1234.0
Harry:5:8769.0

正如你所见,前述代码中 Predicate 和 lambda 表达式的使用使你能够以声明式的方式编写代码。

6.8. 概述

我们以日常生活中的一个例子开始本章的讨论:所有生物都继承其父母的特点和行为,同样的行为(如 繁殖)对不同物种可能有不同的含义。继承使得现有代码可以被重用,并且可以使用类和接口来实现。一个类不能扩展超过一个类,但它可以实现多个接口。继承一个类也称为 子类化,被继承的类被称为 基类父类。继承另一个类或实现接口的类被称为 派生类子类

正如使用姓氏或家族名称称呼某人一样,派生类的对象可以用基类或它实现的接口的变量来引用。但是,当你使用基类的变量来引用对象时,该变量只能访问在基类中定义的成员。同样,类型为接口的变量只能访问在该接口中定义的成员。即使有这种限制,你也可能希望使用基类的变量来引用对象,以便与具有共同基类的多个对象一起工作。

相关类的对象——那些具有继承关系的对象——可以被转换为另一个对象。当你希望通过引用对象的变量访问默认情况下不可用的成员时,你可能希望进行类型转换。

关键字 thissuper 是对象引用,分别用于访问对象及其基类。你可以使用关键字 this 来访问类的变量、方法和构造函数。同样,关键字 super 用于访问基类的变量、方法和构造函数。

多态是对象执行在超类或基类中定义的方法的能力,这取决于它们的类型。具有继承关系的类表现出多态性。多态方法应在基类和继承类中定义。

你可以通过使用类或接口来实现多态。在类的多态情况下,基类可以是抽象类或具体类。这里的方法也不一定是 abstract 方法。当你使用接口实现多态时,你必须使用接口中的 abstract 方法。

Java 8 允许你以声明性方式编写代码。我们涵盖了 lambda 表达式、它们的语法以及 Predicate 接口。

6.9. 复习笔记

类的继承:

  • 一个类可以继承另一个类的属性和行为。

  • 一个类可以实现多个接口。

  • 一个接口可以继承零个或多个接口。一个接口不能继承一个类。

  • 继承使你能够使用现有代码。

  • 继承一个类也称为子类化。

  • 继承另一个类的类被称为派生类或子类。

  • 继承的类被称为父类或基类或超类。

  • 基类的私有成员不能在派生类中被继承。

  • 如果基类和派生类在同一个包中,派生类只能继承具有默认访问修饰符的成员。

  • 一个类使用关键字 extends 来继承一个类。

  • 一个类使用关键字 implements 来实现一个接口。

  • 一个类可以实现多个接口,但不能继承多个类。

  • 一个 abstract 类可以继承一个具体类,一个具体类也可以继承一个 abstract 类。

使用接口:

  • 接口使用关键字 extends 来继承另一个接口。

  • 一个接口可以继承多个接口。

  • 虽然在 Java 8 中接口可以为它们的方法定义默认实现,但这不是强制的。接口也可以定义 abstract 方法。

  • 接口的声明不能包含类名。一个接口永远不能继承任何类。

  • 所有的顶级 Java 类型(类、枚举和接口)只能使用两种访问级别进行声明:public 和 default。内部或嵌套类型可以使用任何访问级别进行声明。

  • strictfp 关键字保证了所有平台上的所有浮点计算结果都是相同的。

  • 接口方法隐式地是抽象的。要定义默认或 static 方法,必须使用关键字 defaultstatic

  • 接口方法可以通过使用默认方法来定义一个实现。

  • 接口中的 static 方法不能使用引用变量来调用。它必须使用接口名称来调用。

  • 与接口不同,如果你在基类中定义了一个 static 方法,它可以通过引用变量或类名来访问。

  • 你必须使用显式访问修饰符 public 来实现接口中的 abstract 方法。

  • 在覆盖默认方法时,你不能使用关键字 default。覆盖默认方法和常规方法的规则是相同的。

  • 类中的static方法和它实现的接口之间没有关系。类中的static方法不会隐藏或覆盖它实现的接口中的static方法。

  • 由于派生类可能从多个基类继承对同一方法签名的不同实现,Java 不允许多重继承。

  • 如果遵循一组规则,一个类可以扩展多个接口。

    • 一个类只有在引用常量不模糊的情况下,才能实现具有相同常量名称的多个接口。

    • 一个类如果具有相同的签名或形成一组重载的方法集,则可以实现对具有相同abstract方法名称的多个接口的实现。

    • 如果一个类覆盖了其默认实现,则它可以实现对具有相同默认方法名称的多个接口的实现。

    • 一个类可以实现对具有相同static方法名称的多个接口的实现,无论它们的返回类型或签名如何。

  • 当一个接口扩展多个接口时,Java 确保它不会为同一方法继承多个方法实现。

  • 接口可以扩展多个定义了具有相同名称的static方法的接口;这些方法的签名无关紧要。

  • 接口中的变量默认是公开的、最终的和静态的。

  • 接口中的方法默认是公开的。

  • 接口不能定义任何构造函数。

引用变量和对象类型:

  • 通过继承,你也可以使用基类或接口的变量来引用派生类的对象。

  • 不能使用派生类的引用变量来引用基类对象。

  • 当一个对象被基类的引用变量引用时,该引用变量只能访问在基类中定义的变量和成员。

  • 当一个对象被实现接口的类的引用变量引用时,该引用变量只能访问接口中定义的变量和方法。

  • 你可能需要使用基类的引用变量来访问派生类的对象,以便将具有共同父类或接口的所有类分组和使用。

转型的需求:

  • 转型是将变量强制表现为另一种类型变量的过程。

  • 如果类Manager扩展了类Employee,并且使用类型为Employee的引用变量emp来引用类Manager的对象,((Manager)emp)将把变量emp转型为Manager

使用superthis来访问对象和构造函数:

  • 关键字superthis是对象引用。这些变量由 JVM 为内存中的每个对象定义和初始化。

  • this引用始终指向对象的自身实例

  • 你可以使用关键字this来引用类可访问的所有方法和变量。

  • 如果一个方法定义了一个与实例变量具有相同名称的局部变量或方法参数,则必须在方法中使用this关键字来访问实例变量。

  • 你可以使用this关键字从一个构造函数调用另一个构造函数。

  • 使用 Java 8,你可以在默认方法中使用this关键字来访问接口的方法和常量。

  • 接口的静态方法不能使用this关键字访问。

  • super,一个对象引用,指向类的父类或基类。

  • 如果存在名称冲突,引用变量super可以用来访问基类中的变量或方法。这种情况通常发生在派生类定义了与基类中相同的变量和方法时。

  • 引用变量super也可以用来在派生类中引用直接父类的构造函数。

类的多态:

  • “多态”一词的字面意思是“多种形式”。

  • 在 Java 中,当类之间存在继承关系,并且基类和派生类都定义了具有相同名称的方法时,就会出现多态。

  • 多态方法也称为覆盖方法。

  • 覆盖方法应定义具有相同名称、相同参数列表和相同方法参数列表的方法。覆盖方法的返回类型可以是相同的,也可以是基类中覆盖方法的返回类型的子类,这称为协变返回类型。

  • 覆盖方法的访问修饰符可以是同等或更少的限制,但不能比被覆盖的方法更严格。

  • 如果派生类定义了一个与基类中相同名称、相同参数列表和相同返回类型的方法,则称派生类覆盖了基类中的方法。

  • 如果在基类中定义的方法在派生类中被重载,那么这两个方法(基类和派生类中的方法)不是多态方法。

  • 当使用类实现多态时,基类中定义的方法可能是abstract的,也可能不是。

  • 当使用接口实现多态时,基接口中定义的方法可以是abstract方法,也可以是非abstract方法,并具有默认实现。

  • 接口中的Static方法不参与多态。

简单的 lambda 表达式:

  • Lambda 表达式仅与功能接口一起使用——定义了恰好一个abstract方法的接口。

  • 每个 lambda 表达式都有多个可选和必填部分:

    • 参数类型(可选)

    • 参数名称(必填)

    • 箭头(必填)

    • 花括号(可选)

    • 关键字return(可选)

    • Lambda 体(必填)

6.10. 样例考试问题

Q6-1.

以下代码的输出是什么?

class Animal {
    void jump() { System.out.println("Animal"); }
}
class Cat extends Animal {
    void jump(int a) { System.out.println("Cat"); }
}
class Rabbit extends Animal {
    void jump() { System.out.println("Rabbit"); }
}
class Circus {
    public static void main(String args[]) {
        Animal cat = new Cat();
        Rabbit rabbit = new Rabbit();
        cat.jump();

        rabbit.jump();
    }
}
  1. Animal
    Rabbit
    
  2. Cat
    Rabbit
    
  3. Animal
    Animal
    
  4. 以上皆非

Q6-2.

给定以下代码,选择正确的语句:

class Flower {
    public void fragrance() {System.out.println("Flower"); }
}
class Rose {
    public void fragrance() {System.out.println("Rose"); }
}
class Lily {
    public void fragrance() {System.out.println("Lily"); }
}
class Bouquet {
    public void arrangeFlowers() {
        Flower f1 = new Rose();
        Flower f2 = new Lily();
        f1.fragrance();
    }
}
  1. 代码的输出是

  2. Flower
    
  3. 代码的输出是

  4. Rose
    
  5. 代码的输出是

  6. Lily
    
  7. 代码无法编译。

Q6-3.

检查以下代码,并选择正确的//INSERT CODE HERE处的代码声明:

interface Movable {
    void move();
}

class Person implements Movable {
    public void move() { System.out.println("Person move"); }
}
class Vehicle implements Movable {
    public void move() { System.out.println("Vehicle move"); }
}
class Test {
    // INSERT CODE HERE
        movable.move();
    }
}
  1. void walk(Movable movable) {
  2. void walk(Person movable) {
  3. void walk(Vehicle movable) {
  4. void walk() {

Q6-4.

选择正确的陈述:

  1. 只有abstract类可以作为基类,通过类实现多态。
  2. 多态方法也被称为重写方法。
  3. 在多态中,根据对象的精确类型,JVM 在编译时执行适当的方法。
  4. 以上皆非。

Q6-5.

给定以下代码,选择正确的陈述:

class Person {}
class Employee extends Person {}
class Doctor extends Person {}
  1. 代码通过类展示了多态性。
  2. 代码通过接口展示了多态性。
  3. 代码通过类和接口展示了多态性。
  4. 以上皆非。

Q6-6.

以下哪些陈述是正确的?

  1. 继承使您能够重用现有代码。
  2. 继承使您免于在多个类中修改通用代码。
  3. 多态向编译器传递特殊指令,以便代码可以在多个平台上运行。
  4. 多态方法不能抛出异常。

Q6-7.

给定以下代码,以下哪些选项是正确的?

class Satellite {
    void orbit() {}
}
class Moon extends Satellite {
    void orbit() {}
}
class ArtificialSatellite extends Satellite {
    void orbit() {}
}
  1. SatelliteMoonArtificialSatellite类中定义的orbit方法是多态的。
  2. 只有在SatelliteArtificial-Satellite类中定义的orbit方法是多态的。
  3. 只有在ArtificialSatellite类中定义的orbit方法是多态的。
  4. 以上皆非。

Q6-8.

检查以下代码:

class Programmer {
    void print() {
        System.out.println("Programmer - Mala Gupta");
    }
}
class Author extends Programmer {
    void print() {
        System.out.println("Author - Mala Gupta");
    }
}
class TestEJava {
    Programmer a = new Programmer();
    // INSERT CODE HERE
    a.print();
    b.print();
}

以下哪行代码可以单独插入到//INSERT CODE HERE,以使代码的输出如下?

Programmer - Mala Gupta
Author - Mala Gupta
  1. Programmer b = new Programmer();
  2. Programmer b = new Author();
  3. Author b = new Author();
  4. Author b = new Programmer();
  5. Programmer b = ((Author)new Programmer());
  6. Author b = ((Author)new Programmer());

Q6-9.

给定以下代码,以下哪个选项单独应用时可以使代码成功编译?

Line1>    interface Employee {}
Line2>    interface Printable extends Employee {
Line3>        String print();
Line4>    }
Line5>    class Programmer {
Line6>        String print() { return("Programmer - Mala Gupta"); }
Line7>    }
Line8>    class Author extends Programmer implements Printable, Employee {
Line9>        String print() { return("Author - Mala Gupta"); }
Line10>   }
  1. 在第 2 行将代码修改为interface Printable{
  2. 在第 3 行将publicStringprint();修改为public void print();
  3. 在第 6 行和第 9 行将print方法的可访问性定义为public
  4. 在第 8 行修改代码,使其仅实现Printable接口。

Q6-10.

以下代码的输出是什么?

class Base {
    String var = "EJava";
    void printVar() {
        System.out.println(var);
    }
}
class Derived extends Base {
    String var = "Guru";
    void printVar() {
        System.out.println(var);
    }
}
class QReference {
    public static void main(String[] args) {
        Base base = new Base();
        Base derived = new Derived();
        System.out.println(base.var);
        System.out.println(derived.var);
        base.printVar();
        derived.printVar();
    }
}
  1. EJava
    EJava
    EJava
    Guru
    
  2. EJava
    Guru
    EJava
    Guru
    
  3. EJava
    EJava
    EJava
    EJava
    
  4. EJava
    Guru
    Guru
    Guru
    

6.11. 样本考试问题的答案

Q6-1.

以下代码的输出是什么?

class Animal {
    void jump() { System.out.println("Animal"); }
}
class Cat extends Animal {
    void jump(int a) { System.out.println("Cat"); }
}
class Rabbit extends Animal {
    void jump() { System.out.println("Rabbit"); }
}
class Circus {
    public static void main(String args[]) {
        Animal cat = new Cat();
        Rabbit rabbit = new Rabbit();
        cat.jump();
        rabbit.jump();
    }
}
  1. Animal
    Rabbit
    
  2. Cat
    Rabbit
    
  3. Animal
    Animal
    
  4. 以上皆非

答案:a

说明:尽管 CatRabbit 类看起来重写了 jump 方法,但 Cat 类没有重写 Animal 类中定义的 jump() 方法。Cat 类定义了一个带有 jump 方法的参数,这使得它成为一个重载方法,而不是重写方法。因为 Cat 类扩展了 Animal 类,它有权访问以下两个重载的 jump 方法:

void jump() { System.out.println("Animal"); }
void jump(int a) { System.out.println("Cat"); }

以下代码行创建了一个 Cat 类的实例并将其赋值给一个 Animal 类型的变量:

Animal cat = new Cat();

当你调用先前对象的 jump 方法时,它执行了没有接受任何方法参数的 jump 方法,并打印以下值:

Animal

以下代码将打印 Animal 而不是 Cat

Cat cat = new Cat();
cat.jump();

Q6-2.

给定以下代码,选择正确的语句:

class Flower {
    public void fragrance() {System.out.println("Flower"); }
}
class Rose {
    public void fragrance() {System.out.println("Rose"); }
}
class Lily {
    public void fragrance() {System.out.println("Lily"); }
}
class Bouquet {
    public void arrangeFlowers() {
        Flower f1 = new Rose();
        Flower f2 = new Lily();
        f1.fragrance();
    }
}
  1. 代码的输出是

  2. Flower
    
  3. 代码的输出是

  4. Rose
    
  5. 代码的输出是

  6. Lily
    
  7. 代码无法编译。

答案:d

说明:尽管代码似乎使用类实现了多态,但请注意,RoseLily 类都没有扩展 Flower 类。因此,类型为 Flower 的变量不能用来存储 RoseLily 类的实例。以下代码行将无法编译:

Flower f1 = new Rose();
Flower f2 = new Lily();

Q6-3.

检查以下代码,并选择在 //INSERT CODE HERE 处插入的正确方法声明:

interface Movable {
    void move();
}
class Person implements Movable {
    public void move() { System.out.println("Person move"); }
}
class Vehicle implements Movable {
    public void move() { System.out.println("Vehicle move"); }
}
class Test {
    // INSERT CODE HERE
        movable.move();
    }
}
  1. void walk(Movable movable) {
  2. void walk(Person movable) {
  3. void walk(Vehicle movable) {
  4. void walk() {

答案:a, b, c

说明:你需要在 Test 类中插入代码,使得以下代码行能够工作:

movable.move();

因此,选项 (d) 是不正确的。因为 Test 类没有定义任何实例方法,所以问题中的代码行能够执行的唯一方式是将方法参数 movable 传递给 walk 方法。

选项 (a) 是正确的。因为接口 Movable 定义了方法 move,所以你可以将它的类型变量传递给 move 方法。

选项 (b) 是正确的。因为类 Person 实现了接口 Movable 并定义了方法 move,所以你可以将它的类型变量传递给 walk 方法。使用这个版本的 walk 方法,你可以传递一个 Person 类的实例或其任何子类的实例。

选项 (c) 是正确的。因为类 Vehicle 实现了接口 Movable 并定义了方法 move,所以你可以将它的类型变量传递给 walk 方法。使用这个版本的 walk 方法,你可以传递一个 Vehicle 类的实例或其任何子类的实例。

Q6-4.

选择正确的语句:

  1. 只有 abstract 类可以作为基类来实现类之间的多态。
  2. 多态方法也称为重写方法。
  3. 在多态中,根据对象的精确类型,JVM 在编译时执行适当的方法。
  4. 以上皆非。

答案:b

选项(a)是不正确的。为了使用类实现多态,可以使用abstract类或具体类作为基类。

选项(c)是不正确的。首先,编译时不会执行任何代码。代码只能在运行时执行。在多态中,确定要执行的确切方法被推迟到运行时,并且由需要调用方法的对象的确切类型决定。

Q6-5.

给定以下代码,选择正确的陈述:

class Person {}
class Employee extends Person {}
class Doctor extends Person {}
  1. 代码展示了类多态性。
  2. 代码展示了接口的多态性。
  3. 代码展示了类和接口的多态性。
  4. 以上都不正确。

答案:d

解释:给定的代码在Person类中没有定义任何在EmployeeDoctor类中被重新定义或实现的方法。尽管EmployeeDoctor类扩展了Person类,但所有三个多态概念或设计原则都是基于方法的,而这些类中缺少这种方法。

Q6-6.

以下哪个陈述是正确的?

  1. 继承使你能够重用现有代码。
  2. 继承可以让你避免在多个类中修改公共代码。
  3. 多态向编译器传递特殊指令,以便代码可以在多个平台上运行。
  4. 多态方法不能抛出异常。

答案:a, b

解释:选项(a)是正确的。继承允许你通过扩展一个类来重用现有代码。这样,在基类中已经定义的功能不需要在派生类中重新定义。基类提供的功能可以在派生类中访问,就像它是在派生类中定义的一样。

选项(b)是正确的。公共代码可以放在基类中,所有派生类都可以扩展它。如果需要对这部分公共代码进行修改,可以在基类中进行修改。修改后的代码将对所有派生类可用。

选项(c)是不正确的。多态不会向编译器传递任何特殊指令以使 Java 代码在多个平台上执行。Java 代码可以在多个平台上执行,因为 Java 编译器编译成虚拟机代码,这是平台无关的。不同的平台实现了这个虚拟机。

选项(d)是不正确的。多态方法可以抛出异常。

Q6-7.

给定以下代码,哪些选项是正确的?

class Satellite {
    void orbit() {}
}
class Moon extends Satellite {
    void orbit() {}
}
class ArtificialSatellite extends Satellite {
    void orbit() {}
}
  1. SatelliteMoonArtificial-Satellite类中定义的orbit方法是多态的。
  2. 只有在SatelliteArtificial-Satellite类中定义的orbit方法是多态的。
  3. 只有在ArtificialSatellite类中定义的orbit方法是多态的。
  4. 以上都不正确。

答案:a

解释:所有这些选项都定义了类。当在具有继承关系的类中定义具有相同方法签名的类时,这些方法被认为是多态的。

Q6-8.

检查以下代码:

class Programmer {
    void print() {
        System.out.println("Programmer - Mala Gupta");
    }
}
class Author extends Programmer {
    void print() {
        System.out.println("Author - Mala Gupta");
    }
}
class TestEJava {
    Programmer a = new Programmer();
    // INSERT CODE HERE
    a.print();
    b.print();
}

以下哪行代码可以单独插入到 //INSERT CODE HERE 以使代码的输出如下?

Programmer - Mala Gupta
Author - Mala Gupta
  1. Programmer b = new Programmer();
  2. Programmer b = new Author();
  3. Author b = new Author();
  4. Author b = new Programmer();
  5. Programmer b = ((Author)new Programmer());
  6. Author b = ((Author)new Programmer());

答案:b, c

解释:选项 (a) 是错误的。此代码可以编译,但由于引用变量和对象都是 Programmer 类型,调用此对象的 print 方法将打印 Programmer - Mala Gupta,而不是 Author - Mala Gupta

选项 (d) 是错误的。不能将基类对象赋值给派生类引用变量。

选项 (e) 是错误的。此行代码可以成功编译,但在运行时将因 ClassCastException 而失败。基类对象不能被转换为派生类对象。

选项 (f) 是错误的。表达式 ((Author)new Programmer()) 在被分配给 Author 类型的引用变量之前就被评估了。此行代码也尝试将基类对象 Programmer 转换为其派生类对象 Author。此代码也可以成功编译,但在运行时将因 ClassCastException 而失败。使用 Author 类型的引用变量在这里没有区别。这里重要的是 new 操作符后面的类型。

Q6-9.

给定以下代码,以下哪个选项单独应用后可以使代码成功编译?

Line1>    interface Employee {}
Line2>    interface Printable extends Employee {
Line3>        String print();
Line4>    }
Line5>    class Programmer {
Line6>        String print() { return("Programmer - Mala Gupta"); }
Line7>    }
Line8>     class Author extends Programmer implements Printable, Employee {
Line9>        String print() { return("Author - Mala Gupta"); }
Line10>    }
  1. 将第 2 行的代码修改为 interface Printable {
  2. 将第 3 行的代码修改为 public String print();
  3. 在第 6 行和第 9 行将 print 方法的访问权限定义为 public
  4. 将第 8 行的代码修改为仅实现接口 Printable

答案:c

解释:接口中的方法默认是 public 的。实现接口的非 abstract 类必须实现接口中定义的所有方法。在重写或实现方法时,实现的方法的访问权限必须是 public。重写的方法不能被赋予比 public 弱的访问权限。

选项 (a) 是错误的。接口 Printable 扩展接口 Employee 以及类 Author 实现这两个接口时没有问题。

选项 (b) 是错误的。在行 3 上添加访问修饰符到 print 方法对现有代码没有任何影响。接口中定义的方法默认是 public 的。

选项 (d) 是错误的。当一个接口扩展另一个接口时,类实现两个接口没有问题。

Q6-10.

以下代码的输出是什么?

class Base {
    String var = "EJava";
    void printVar() {
        System.out.println(var);
    }
}
class Derived extends Base {
    String var = "Guru";
    void printVar() {
        System.out.println(var);
    }
}
class QReference {
    public static void main(String[] args) {

        Base base = new Base();
        Base derived = new Derived();
        System.out.println(base.var);
        System.out.println(derived.var);
        base.printVar();
        derived.printVar();
    }
}
  1. EJava
    EJava
    EJava
    Guru
    
  2. EJava
    Guru
    EJava
    Guru
    
  3. EJava
    EJava
    EJava
    EJava
    
  4. EJava
    Guru
    Guru
    Guru
    

答案:a

解释:在继承中,实例变量在编译时绑定,而方法在运行时绑定。以下代码行使用类型为 Base 的引用变量引用了 Base 类的对象。因此,以下两行代码都打印 EJava

System.out.println(base.var);
base.printVar();

但以下代码行使用类型为 Base 的引用变量引用了 Derived 类的对象:

Base derived = new Derived();

因为实例变量在编译时绑定,所以以下代码行访问并打印了在 Base 类中定义的实例变量的值:

System.out.println(derived.var);    // prints EJava

derived.printVar() 中,尽管使用类型为 Base 的引用调用了 printVar 方法,但 JVM 知道该方法是在 Derived 对象上调用的,因此执行了 Derived 类中重写的 printVar 方法。

第七章. 异常处理

本章涵盖的考试目标 你需要了解的内容
[8.3] 描述异常处理的优点。 异常处理器需求和优点。
[8.1] 区分检查型异常、非检查型异常和错误。 检查型异常、RuntimeExceptions 和错误之间的差异和相似之处。这些异常和错误在代码中处理方式上的差异和相似之处。
[8.2] 创建 try-catch 块并确定异常如何改变正常程序流程。 如何创建 try-catch-finally 块。当封装的代码抛出异常或错误时的代码流程。如何创建嵌套的 try-catch-finally 块。
[8.4] 创建并调用一个抛出异常的方法。 如何创建抛出异常的方法。覆盖或被覆盖的方法抛出或未抛出异常的规则。当调用方法抛出异常时,如何确定控制流的流程。如何将此应用于没有 try 块抛出异常以及从 try 块(带有适当的和不足的异常处理器)抛出的情况。抛出或未抛出异常的方法调用差异。
[8.5] 识别常见的异常类(如 NullPointerException、Arithmetic-Exception、ArrayIndexOutOfBounds-Exception、ClassCastException) 如何识别可能抛出这些异常的代码,并适当地处理它们。

想象一下,你即将登机前往日内瓦参加一个重要的会议。在最后一刻,你得知航班已被取消,因为飞行员感觉不适。幸运的是,航空公司迅速安排了备用飞行员,使得航班能够按照原定时间起飞。多么令人欣慰啊!

这个例子说明了异常条件如何可以修改动作的初始流动,并展示了适当处理这些条件的必要性。在 Java 中,一个异常条件(如飞行员生病)可能会影响正常的代码流程(航空公司航班运营)。在这种情况下,为备用飞行员所做的安排可以比作异常处理器。

根据异常条件的性质,你可能能够完全恢复,也可能不能。例如,如果地震破坏了大部分机场,航空公司管理能否让你的航班起飞?

在考试中,你将针对 Java 代码和异常被问及类似的问题。考虑到这一点,本章涵盖了以下内容:

  • 理解和识别代码中出现的异常

  • 确定异常如何改变正常程序流程

  • 在你的代码中单独处理异常的需求

  • 使用try-catch-finally块处理异常

  • 区分检查型异常、非检查型异常和错误

  • 调用可能抛出异常的方法

  • 识别常见的异常类别和类

你可能会觉得本章内容很多,但请记住,我们不会深入探讨太多背景信息,因为我假设你已经知道类和方法、类继承、数组和ArrayList的定义和用法。本章的重点是考试目标和关于异常你需要了解的内容。

在本章中,我不会讨论带有多个catch子句的try语句、使用try-with-resources 语句自动关闭资源或创建自定义异常。这些主题将在 Java 认证的下一级别(在 OCP Java SE 8 程序员 II 级考试中)介绍。

7.1. Java 中的异常

[8.3] 描述异常处理的优点

在本节中,你将了解 Java 中的异常是什么,为什么需要将异常处理与主代码分开,以及它们的优缺点。

7.1.1. 体验异常

在图 7.1 中,你认为在ArrayAccessOpenFileMethodAccess类中加粗的代码有什么共同点吗?

图 7.1. 体验 Java 中的异常

图片

我确信,鉴于本章的标题,这个问题很容易回答。这三个陈述都与抛出异常或错误相关。让我们分别看看它们:

  • ArrayAccess—由于数组students的长度是3,尝试访问数组位置5的元素是一个异常条件,如图 7.2 所示。

    图 7.2. ArrayIndexOutOfBoundsException的例子

    图片

  • OpenFileFileInputStream类的构造函数抛出一个检查异常FileNotFoundException(如图 7.3 所示)。如果你尝试在不将其包含在try块中捕获它,或者标记为由main方法抛出(使用throws语句),或者捕获这个异常的情况下编译此代码,你的代码将无法编译。(我将在 7.2.3 节中详细讨论检查异常。)

    图 7.3. FileNotFoundException的例子

    图片

  • MethodAccess—如图 7.4 所示,myMethod方法递归调用自身,没有指定退出条件。这些递归调用在运行时导致StackOverflowError

    图 7.4. StackOverflowError的例子

    图片

这些异常的例子在 OCA Java SE 8 程序员 I 级考试中很典型。让我们继续前进,探索 Java 中的异常及其处理,以便你能识别出抛出异常的代码并相应地处理它们。

Java 中的文件 I/O

文件 I/O 在本考试中不涉及,但你可能会在有关异常处理的问题中看到它被提及。在这里,我将简要介绍它,仅限于本考试所需。

文件 I/O 涉及多个类,这些类使你能够从源读取数据并将其写入。这个数据源可以是持久存储、内存,甚至是网络连接。数据可以作为二进制或字符数据的流来读取和写入。一些文件 I/O 类只从源读取数据,一些将数据写入源,还有一些两者都做。

在本章中,您将使用文件 I/O API 中的三个类:java.io.Filejava.io.FileInputStreamjava.io.FileOutputStreamFile是对文件和目录路径名的抽象表示。您可以打开一个File,然后从中读取和写入。FileInputStream使用File类的对象获取输入字节。它定义了read方法来读取字节和close方法来关闭此流。FileOutputStream是用于将数据写入File的输出流。它定义了write方法来写入字节和close方法来关闭此流。

创建FileInputStreamFileOutputStream类的对象可能会抛出检查型异常java.io.FileNotFoundException。在FileInputStreamFileOutputStream类中定义的readwriteclose方法可能会抛出检查型异常java.io.IOException。请注意,FileNotFoundExceptionIOException的子类。

7.1.2. 为什么单独处理异常?

假设您想在博客网站上发布一些评论。要发表评论,您必须完成以下步骤:

  1. 访问博客网站。

  2. 登录您的账户。

  3. 选择您想要评论的博客。

  4. 发布您的评论。

上述列表可能看起来像是一组理想的步骤。在实际情况下,你可能必须验证是否完成了前面的步骤,然后才能进行下一步。图 7.5修改了前面的步骤。

图 7.5. 在没有单独的异常处理程序的情况下,对抗异常条件时丢失的预期代码流程

图 7.5

修改后的逻辑(图 7.5)要求代码在用户继续下一步之前检查条件。这种在多个地方检查条件引入了新的步骤,也引入了原始步骤的新执行路径。这些修改后的路径的难点在于,它们可能会使用户对试图完成的任务中涉及的步骤感到困惑。图 7.6 展示了异常处理如何有所帮助。

图 7.6. 将异常处理代码与主代码逻辑分开定义

图 7.6

图 7.6 中的代码定义了发布博客评论所需的原始步骤,以及一些异常处理代码。由于异常处理程序是单独定义的,因此关于您需要完成哪些步骤才能在网站上发布评论的任何混淆都得到了澄清。此外,由于适当的异常处理程序,此代码在移动到下一步之前检查步骤完成的情况,没有妥协。

7.1.3. 异常处理提供其他好处吗?

除了在定义常规程序逻辑和异常处理代码之间分离关注点之外,异常还可以通过提供异常或错误的堆栈跟踪来帮助定位有问题的代码(抛出异常的代码),以及定义它的方法。

注意

堆栈跟踪 被称为这样是因为它提供了一种回溯堆栈的方法——生成错误的方法调用序列(在 7.2 节 中详细说明)。

这里有一个例子:

public class Trace {                                   // line 1
    public static void main(String args[]) {           // line 2
        method1();                                     // line 3
    }                                                  // line 4
    public static void method1() {                     // line 5
        method2();                                     // line 6
    }                                                  // line 7

    public static void method2() {                     // line 8
        String[] students = {"Shreya", "Joseph"};      // line 9
        System.out.println(students[5]);               // line 10
    }                                                  // line 11
}                                                      // line 12

method2() 尝试访问 students 数组在索引 5 的元素,这是 students 数组的无效索引,因此代码在运行时抛出 ArrayIndexOutOfBoundsException 异常。图 7.7 展示了抛出此异常时的堆栈跟踪。它包括运行时异常消息和调用抛出异常的代码的方法列表,从应用程序的入口点 main 方法开始。您可以将 图 7.7 中的堆栈跟踪指定的行号与代码中的行号进行匹配。

图 7.7. 追踪运行时抛出异常的代码行

注意

堆栈跟踪提供了 JVM 遇到未处理的异常时调用的方法的跟踪。堆栈跟踪是从下往上读取的。在 图 7.7 中,跟踪从 main 方法(堆栈跟踪的最后一行)开始,一直延续到包含抛出异常的代码的方法。根据您代码的复杂性,堆栈跟踪可以从几行到几百行代码不等。堆栈跟踪与已处理和未处理的异常都兼容。

让我们继续前进,看看异常传播的更多细节,以及创建 try-catch-finally 块来处理代码中的异常。

在深入了解异常处理之前,让我们看看异常的多种形式。

7.2. 异常的分类

[8.1] 区分检查型异常、非检查型异常和错误

Java 编译器和其运行时以不同的方式处理异常类别。这意味着存在不同的规则来定义抛出异常的方法和处理它们的代码。

在本节中,您将了解 Java 中异常的分类:检查型异常、运行时异常和错误。

7.2.1. 识别异常类别

如 图 7.8 所示,异常可以分为三大类:

  • 检查型异常

  • 运行时异常

  • 错误

图 7.8. 异常的分类:检查型异常、运行时异常和错误

注意

运行时异常和错误统称为非检查型异常。

在这三种类型中,当涉及到编码和使用方法时,检查异常需要你最多的注意。运行时异常代表编程错误。应该插入检查以防止抛出运行时异常。对于错误,你可以使用的选项很少,因为它们是由 JVM 抛出的。

对于 OCA Java SE 8 程序员 I 考试,对这三种异常类别有一个清晰的理解非常重要,包括它们的相似之处和不同之处。

7.2.2. 异常类别的类层次结构

异常类别相互关联;它们都扩展了 java.lang.Throwable 类(如图 7.9 所示的类层次结构)。

图 7.9. 异常类别的类层次结构

根据它们的类层次结构,以下是异常的分类:

  • 检查异常java.lang.Exception 及其子类(不包括 java.lang.RuntimeException 及其子类)

  • 运行时异常java.lang.RuntimeException 及其子类

  • 错误java.lang.Error 及其子类

让我们详细考察这些类别中的每一个。

7.2.3. 检查异常

当我们谈论异常处理时,检查 异常占据了我们的大部分注意力。

什么是检查异常?

  • 检查异常是指方法作者预见到但无法直接控制的不容接受的条件 预见。例如,FileNotFoundException 是一个检查异常。如果代码尝试访问的文件找不到,就会抛出这个异常。一个方法,比如 readFile(),可以声明在无法访问目标文件时抛出它。

  • 检查异常之所以被称为检查异常,是因为它们在编译时进行检查。如果一个方法调用抛出检查异常,编译器会检查并确保调用方法要么处理该异常,要么声明它将被重新抛出。

  • 检查异常是 java.lang.Exception 类的子类,但它不是 java.lang.RuntimeException 的子类。然而,值得注意的是,java.lang.RuntimeException 类本身是 java.lang.Exception 类的子类。

考试技巧

在这次考试中,你可能需要选择使用哪种类型的引用变量来在处理程序中存储抛出的检查异常的对象。要正确回答这类问题,请记住,检查异常是 java.lang.Exception 的子类,但不是 java.lang.RuntimeException 的子类。

检查异常是 API 的一部分,并且有很好的文档记录。以下是一个快速示例,展示了 Java API 中 java.io.FileInputStream 类构造函数的声明:

public FileInputStream(File file)
                throws FileNotFoundException

受检异常是在编写方法时程序员 预见 的不可接受的条件。通过将这些异常声明为受检异常,方法的作者让用户意识到可能由其使用引起的异常条件。使用带有受检异常的方法的用户必须相应地处理异常条件。

7.2.4. 运行时异常

虽然你将花费大部分的时间和精力来应对受检异常,但运行时异常会给你带来最多的麻烦。这尤其在你准备实际项目时更为明显。一些运行时异常的例子包括 NullPointerException(最常见的一种)、ArrayIndexOutOfBoundsExceptionClassCastException

什么是运行时异常?

  • 运行时异常是编程错误的表示。这些异常通常是由于对代码的不当使用引起的。例如,NullPointerException 是一种运行时异常,当一段代码尝试在一个尚未分配对象且指向 null 的变量上执行代码时发生。另一个例子是 ArrayIndexOutOfBoundsException,当一段代码尝试访问一个不存在位置的数组元素时抛出。

  • 运行时异常之所以被称为运行时异常,是因为在方法执行之前无法确定方法调用是否会抛出运行时异常。

  • 运行时异常是 java.lang.RuntimeException 的子类。

  • 在方法签名中声明运行时异常是可选的。是否显式声明它取决于编写代码的人。

考试技巧

运行时异常和错误统称为非受检异常。

7.2.5. 错误

无论你是准备考试还是实际项目,你都需要知道 JVM 在何时抛出错误。这些错误被认为是 严重 的异常条件,它们不能直接被你的代码控制。

什么是错误?

  • 错误是 JVM 由于处理你的代码的环境状态错误而抛出的严重异常。例如,NoClassDefFound-Error 是 JVM 在无法找到它应该运行的 .class 文件时抛出的错误。StackOverflowError 是 JVM 在 Java 程序所需的栈大小超过 JRE 为 Java 应用程序提供的内存时抛出的另一个错误。这个错误也可能由于无限循环或深度嵌套循环而出现。

  • 错误是 java.lang.Error 类的子类。

  • 错误不必是方法签名的一部分。

  • 错误可以被异常处理器捕获,但通常不应该这样做。

让我们继续创建抛出异常的方法。

7.3. 创建抛出异常的方法

[8.4] 创建并调用抛出异常的方法

在本节中,你将探索创建抛出异常的方法的需求。你还将使用throwthrows关键字来定义抛出异常的方法。

为什么你需要抛出异常的方法?想象一下,你被分配了一个找到特定书籍并阅读和向一班学生解释其内容的任务。所需的顺序如下:

  1. 获取指定的书籍。

  2. 大声朗读其内容。

  3. 向一班学生解释内容。

但如果你找不到指定的书籍会发生什么?没有它,你无法继续执行其他操作,因此你需要向分配任务给你的人报告。这个意外事件(缺失的书籍)阻止了你完成任务。通过报告它,你希望这个请求的发起者采取纠正或替代措施。

让我们将前面的任务编码为teachClass方法,如图 7.10 所示,它使用了throw语句和throws子句。此示例代码仅用于演示目的,因为它使用了BookNotFoundException异常以及locateBook()readBook()explainContents()方法,这些方法尚未定义。

图 7.10. 使用throwthrows创建可以抛出异常的方法

图片

图 7.10 中的代码易于理解。在执行throw new BookNotFoundException()代码时,teachClass()的执行将停止。JVM 创建了一个BookNotFoundException的实例,并将其发送给teachClass()的调用者,以便做出替代安排。

throw语句用于抛出BookNotFoundException的实例。throws语句用于teachClass()方法的声明中,以表示它可以抛出BookNotFoundException

为什么方法选择抛出异常而不是自己处理?这是调用方法与被调用方法之间的契约。参考图 7.9 中显示的teachClass()方法,teachClass的调用者希望如果teachClass()无法找到指定的书籍,能够得到通知。teachClass()方法不处理BookNotFoundException,因为它的职责不包括处理缺失的书籍。

前面的例子有助于确定你希望方法抛出异常而不是自己处理的情况。它展示了如何使用和比较throwthrows语句——用于抛出异常以及表示方法可能抛出异常。示例还展示了当被调用方法没有成功完成并抛出异常时,调用方法可以定义替代代码。除了测试这个逻辑之外,考试还将测试你如何创建和使用抛出检查或非检查异常和错误的方法,以及一些其他规则。

7.3.1. 创建一个抛出检查异常的方法

让我们创建一个简单的方法,它不处理由它抛出的已检查异常,使用throwthrows语句。DemoThrowsException类定义了readFile()方法,它在方法声明中包含一个throws子句。实际抛出异常是通过throw语句完成的:

图片

一个方法的throws子句可以包含多个以逗号分隔的异常类名。在方法声明中包含运行时异常或错误不是必需的。在文档中提及它们是首选方式。即使没有在throws子句中包含,方法仍然可以抛出运行时异常或错误。

考试技巧

在语法上,您不一定需要throwthrows语句的组合来创建抛出异常(已检查或未检查)的方法。您可以用抛出异常的方法来替换throw语句。

7.3.2. 处理或声明规则

要使用抛出已检查异常的方法,您必须执行以下操作之一:

  • 处理异常—— 将代码放在try块中,并捕获抛出的异常。

  • 声明抛出—— 使用throws子句声明要抛出的异常。

  • 处理和声明—— 同时实现上述两种选项。

考试技巧

处理或声明异常的规则也被称为处理或声明规则。要使用抛出已检查异常的方法,您必须处理该异常或声明它将被抛出。但此规则仅适用于已检查异常,不适用于未检查异常。

7.3.3. 创建抛出运行时异常或错误的方法

当创建一个抛出运行时异常或错误的方法时,在throws子句中包含异常或错误名称不是必需的。抛出运行时异常或错误的方法不受处理或声明规则的限制。

让我们通过修改前面的示例来观察这个概念的实际应用,使readFile()方法在传入null值时抛出NullPointerException(运行时异常)(本例中代码更改以粗体显示,并在本章的其余部分中显示):

图片

考试可能会通过在一个方法的声明中包含运行时异常和错误的名称,而在另一个方法中省略它们来欺骗您。(您可以在throws子句中包含未检查异常的名称,但不必这样做。)假设其余代码保持不变,以下方法声明是正确的:

图片

考试技巧

在方法声明中添加运行时异常或错误不是必需的。无论其名称是否包含在throws子句中,方法都可以抛出运行时异常或错误。

7.3.4. 一个方法可以声明抛出所有类型的异常,即使它没有

在下面的示例中,ThrowExceptions类定义了多个方法,这些方法声明抛出不同的异常类型。即使ThrowExceptions类的方法不包含可能抛出这些异常的代码,该类也能成功编译:

class ThrowExceptions {
    void method1() throws Error {}
    void method2() throws Exception {}
    void method3() throws Throwable {}
    void method4() throws RuntimeException {}
    void method5() throws FileNotFoundException {}
}

虽然一个try块可以定义一个处理它未抛出的未检查异常的处理程序,但它不能为检查异常(除了Exception)这样做:

图片描述

在前面的代码中,method6()method7()method8()method9()即使它们的try块没有定义代码来抛出由其catch块处理的异常,也能编译。但method10()不能编译。

考试技巧

一个方法可以声明抛出任何类型的异常,无论是检查的还是未检查的,即使它没有这样做。但是,如果try块没有抛出该检查异常或使用声明抛出该检查异常的方法,则try块不能为检查异常(除了Exception)定义一个catch块。

在下一节中,我们将详细说明抛出异常时会发生什么以及如何处理它。

7.4. 抛出异常时会发生什么?

[8.2] 创建一个 try-catch 块并确定异常如何改变正常程序流程

[8.4] 创建并调用一个抛出异常的方法

在本节中,我们将揭示在 Java 中抛出异常时会发生什么。我们将通过几个示例来了解当抛出异常时,代码的正常流程是如何被打断的。我们还将定义一个使用try-catch-finally块的替代程序流程,以处理可能抛出异常的代码。

与所有其他 Java 对象一样,异常是一个对象。所有类型的异常都是java.lang.Throwable的子类。当一段代码遇到以异常条件形式出现的障碍时,它会创建一个java.lang.Throwable类的对象(在运行时,创建一个最合适的子类型对象),并用必要的信息(如其类型、可选的文本描述以及有问题的程序状态)初始化它,然后将它交给 JVM。JVM 通过这个异常发出警报,并寻找可以“处理”这个异常的适当代码块。JVM 记录了它遇到有问题的代码时调用过的所有方法,因此为了找到一个适当的异常处理程序,它会查看所有跟踪的方法调用。

重新审视第 7.1.3 节中提到的Trace类及其抛出的ArrayIndexOutOfBoundsException异常。图 7.11 说明了method2抛出的ArrayIndexOutOfBoundsException异常通过所有方法的传播。

图 7.11. 异常通过多个方法调用的传播

图片描述

要理解异常如何在方法调用中传播,了解方法调用的工作方式很重要。应用程序从 main 方法开始执行,main 可能会调用其他方法。当 main 调用另一个方法时,被调用的方法应该在其完成执行之前,main 才能完成自己的执行。

操作系统(OS)使用一个 来跟踪它需要执行的代码。栈是一种列表,其中最后添加到其中的项目是第一个被取出的——后进先出。这个栈使用一个 栈指针 来指向操作系统应该执行的指令。

现在你已经掌握了这些基本信息,下面将逐步讨论异常传播,如图 7.11 所示:

  1. main 方法开始执行时,其指令被推入栈中。

  2. 方法 main 调用方法 method1,并将 method1 的指令推入栈中。

  3. method1 调用 method2method2 的指令被推入栈中。

  4. method2 抛出一个异常:ArrayIndexOutOfBoundsException。因为 method2 没有自己处理这个异常,所以它被传递给了调用它的方法——method1

  5. method1 没有为 ArrayIndexOutOfBounds-Exception 定义任何异常处理器,所以它将这个异常传递给其调用方法——main

  6. main 中没有为 ArrayIndexOutOfBoundsException 定义异常处理器。因为没有其他方法处理 ArrayIndexOutOfBounds-Exception,所以 Trace 类的执行停止。

你可以使用 try-catch-finally 块来定义当抛出异常时要执行的代码,如下一节所述。

7.4.1. 创建 try-catch-finally 块

当你处理异常处理器时,你经常会听到 trycatchfinally 这些术语。在你开始使用这些概念之前,我会回答三个简单的问题:

  • 尝试什么? 首先,你尝试执行你的代码。如果它没有按计划执行,你将使用 catch 块来处理异常情况。

  • 捕获什么? 你捕获 try 块内代码产生的异常事件,并通过定义适当的异常处理器来处理事件。

  • finally 块的作用是什么? 最后,你执行一组代码,在所有条件下,无论 try 块中的代码是否抛出任何异常。

让我们通过一个现实生活中的例子来比较 try-catch-finally 块。想象一下,你在度假期间去漂流。你的教练告诉你,在穿越急流时,你可能会从船上掉入河中。在这种情况下,你应该尝试使用你的浆或向你扔来的绳子回到船上。在划船的时候,你也可能会把浆掉入河中。在这种情况下,你不应该惊慌,应该保持坐姿。无论发生什么,你都在为这项冒险运动付费。

将此与 Java 代码进行比较:

  • 你可以将划橡皮筏比作一个其方法 可能会 抛出异常的类。

  • 穿越急流和划桨是可能会抛出异常的方法。

  • 从筏上掉下来和丢弃你的桨是异常情况。

  • 回到筏上并且不慌张的步骤是异常处理程序——当出现异常时执行的代码。

  • 无论你是否留在船上,你为这项运动支付的费用可以与 finally 块相比较。

让我们通过定义适当的类和方法来实现之前的现实生活示例。首先,这里有两个基本的异常类——FallInRiver-ExceptionDropOarException——这些异常可以被 RiverRafting 类中的方法抛出:

class FallInRiverException extends Exception {}
class DropOarException extends Exception {}
注意

你可以创建自己的异常——一个自定义异常——通过扩展 Exception 类(或其任何子类)。尽管自定义类的创建不在此考试范围内,你可能会在考试中看到创建和使用自定义异常的问题。也许这些问题的存在是因为 Java API 中几乎没有已检查异常出现在这个考试中。考试中的编码问题可能会创建和使用自定义异常。

以下是对类 RiverRafting 的定义。它的方法 crossRapidrowRaft 可能会抛出类型为 FallInRiverExceptionDropOarException 的异常:

处的 crossRapid 方法抛出异常 FallInRiverException。当你调用此方法时,你应该为此异常定义一个异常处理程序。同样,在 处的 rowRaft 方法抛出异常 DropOarException。当你调用此方法时,你应该为此异常定义一个异常处理程序。

当你执行可能会抛出 已检查异常(不扩展 RuntimeException 类的异常)的方法时,应将代码放在 try 块中。跟在 try 块后面的 catch 块应该处理 try 块中抛出的所有已检查异常(已检查异常在 7.2.3 节 中详细说明)。

图 7.12 中显示的代码使用了之前定义的 RiverRafting 类,并描述了当第 3 行代码(riverRafting.crossRapid(11);)抛出类型为 FallInRiverException 的异常时的控制流程。

图 7.12. 抛出异常时的修改后的控制流程

图 7.12 中的示例显示了异常如何改变正常的程序流程。如果第 3 行代码抛出异常(FallInRiverException),第 4 行和第 5 行代码将不会执行。在这种情况下,控制权转移到处理 FallInRiverException 的代码块。然后控制权转移到 finally 块。在 finally 块执行完毕后,将执行 try-catch-finally 块之后的代码。前述代码的输出如下:

Cross Rapid
Get back in the raft
Pay for the sport
After the try block

如果你将之前的示例代码修改如下,第 3 行的代码将不会抛出异常(粗体部分为修改):

之前代码的输出如下:

Cross Rapid
Row Raft
Enjoy River Rafting
Pay for the sport
After the try block

你认为如果 rowRaft 方法抛出异常,代码的输出会是什么?自己试一试!

考试提示

The finally block executes regardless of whether the try block throws an exception.

单个 try 块,多个 catch 块和一个 finally

对于 try 块,你可以定义多个 catch 块,但只能有一个 finally 块。多个 catch 块用于处理不同类型的异常。finally 块用于定义 清理 代码——关闭和释放资源(如文件句柄、数据库或网络连接)的代码。

当涉及到代码时,通过观察其运行情况来验证一个概念是有意义的。让我们通过一个简单的例子来了解一下如何使用 try-catch-finally 块。

在以下列表中,FileInputStream 类的构造函数可能会抛出 FileNotFoundException,在 FileInputStream 对象(例如 fis)上调用 read 方法可能会抛出 IOException

列表 7.1. 带有多个 catch 语句和 finally 块的代码流程

表 7.1 比较了根据系统是否能够打开(和读取)file.txt 而发生的代码输出。

表 7.1. 当系统无法打开 file.txt 时以及当系统能够打开 file.txt 但无法读取时的代码输出
系统无法打开 file.txt 时的输出 系统能够打开 file.txt 但无法读取时的输出
文件未找到 finally 下一个任务.. 文件已打开 文件关闭 异常 finally 下一个任务..

在 表 7.1 中描述的任何情况下,finally 块都会执行,并在其执行后,控制权转移到 try-catch 块之后的语句。以下是 MultipleExceptions 类在没有代码抛出异常时的输出:

File Opened
Read File
finally
Next task..

现在是时候尝试本章的第一个故事转折练习了。当你执行这个练习中的代码时,你会了解当你更改异常处理程序的位置时会发生什么(答案在附录中)。

故事转折 7.1

让我们修改 列表 7.1 中 finally 块的位置,看看会发生什么。

假设系统上不存在 file.txt 文件,以下代码的输出是什么?

import java.io.*;
public class MultipleExceptions {
    public static void main(String args[]) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("file.txt");
            System.out.println("File Opened");
            fis.read();
            System.out.println("Read File");
        } finally {
            System.out.println("finally");

        } catch (FileNotFoundException fnfe) {
            System.out.println("File not found");
        } catch (IOException ioe) {
            System.out.println("File Closing Exception");
        }
        System.out.println("Next task..");
    }
}
  1. 代码打印

    File not found
    finally
    Next task..
    
  2. 代码打印

    File Opened
    File Closing Exception
    finally
    Next task..
    
  3. 代码打印 文件未找到

  4. 代码无法编译。

7.4.2. 使用抛出检查异常的方法

要使用抛出 检查型异常 的方法,你必须遵循处理或声明规则(第 7.3.2 节)。在以下代码中,类 TestRiverRafting 中的 main 方法无法编译,因为它没有处理或声明由 crossRapid 方法声明的检查型异常 FallInRiverException

图片

HandleDeclareHandleAndDeclare 类中的 main 方法能够成功编译,因为它们遵循了处理或声明规则:

class Handle {
    public static void main(String args[]) {
        RiverRafting riverRafting = new RiverRafting();
        try {
            riverRafting.crossRapid(9);
        } catch (FallInRiverException e) {
            System.out.println("Exception : " + e);
        }
    }
}
class Declare {
    public static void main(String args[]) throws FallInRiverException {
        RiverRafting riverRafting = new RiverRafting();
        riverRafting.crossRapid(9);
    }
}
class HandleAndDeclare {
    public static void main(String args[]) throws FallInRiverException {
        RiverRafting riverRafting = new RiverRafting();
        try {
            riverRafting.crossRapid(9);
        } catch (FallInRiverException e) {
            System.out.println("Exception : " + e);
        }
    }
}
考试技巧

要使用抛出 检查型异常 的方法,你必须遵循处理或声明规则。

7.4.3. 使用抛出运行时异常的方法

如果一个方法抛出运行时异常,则不需要在方法声明中包含异常名称(尽管允许这样做)。要使用抛出运行时异常的方法,你不需要遵循声明或处理规则。以下是一个示例:

图片

这里还有一个例子。检查以下代码,它抛出一个运行时异常(ArrayIndexOutOfBoundsException):

图片

前述代码没有从 System.out.println("一切看起来都很顺利") 打印输出。代码执行因尝试输出 students[5] 的值而抛出的异常而中断。

可以为前述示例代码中抛出的 ArrayIndexOutOfBoundsException 异常创建一个异常处理器,如下所示:

public class InvalidArrayAccess {
    public static void main(String args[]) {
        String[] students = {"Shreya", "Joseph"};
        try {
            System.out.println(students[5]);
        } catch (ArrayIndexOutOfBoundsException e){
            System.out.println("Exception");
        }
        System.out.println("All seems to be well");
    }
}

之前代码的输出如下:

Exception
All seems to be well

同样,你可以 捕获 检查型异常,你也可以捕获 Runtime-Exception。在实际项目中,首选的方法是通过包含适当的检查来避免运行时异常。例如,在之前的代码中,你可以通过使用适当的检查来防止抛出 ArrayIndexOutOfBoundsException

图片

7.4.4. 使用抛出错误的的方法

错误是 JVM 抛出的严重异常,例如当 JVM 内存不足或找不到类的定义时。你不应该定义代码来处理错误。相反,你应该让 JVM 处理错误。

在本节的剩余部分,我们将探讨一些关于 try-catch-finally 块的常见问题,这些问题常常让认证考生感到困惑。

7.4.5. 即使 catch 块定义了 return 语句,finally 块也会执行吗?

想象以下场景:一个男孩承诺为他的女朋友买钻石,并带她喝咖啡。女孩询问如果他在购买钻石时遇到异常情况,比如资金不足,会发生什么。让女孩失望的是,男孩回答说他会仍然带她喝咖啡。

你可以将 try 块比作购买钻石,将 finally 块比作咖啡待遇。无论男孩是否成功购买钻石,女孩都会得到咖啡待遇。图 7.13 展示了这次对话。

图 7.13. 一点幽默来帮助你记住,finally块无论是否抛出异常都会执行

有趣的是,即使try块或任何catch块中的代码定义了return语句,finally块也会执行。检查图 7.14 中的代码及其输出,并注意当类ReturnFromCatchBlock无法打开 file.txt 时。

图 7.14. 即使异常处理器定义了return语句,finally块也会执行。

从图 7.14 的代码输出中可以看出,当在FileNotFoundExceptioncatch处理器中执行return语句时,控制流不会返回到main方法。它继续执行finally块,然后再将控制权转回到main方法。注意,控制权不会转移到try块后面的println语句"Next task.. ",因为如前所述,在catch块中遇到了return语句。

回到那个男生和他女朋友的例子,一些悲剧性的条件,比如地震或龙卷风,可以取消咖啡待遇。同样,在 Java 中也有一些场景,finally块不会执行:

  • 应用程序终止—trycatch块执行System.exit,立即终止应用程序。

  • 致命错误— JVM 或操作系统崩溃。

在考试中,你可能会被问到两个或多个异常处理器的正确顺序。顺序重要吗?看看你自己第 7.4.9 节。

7.4.6. 如果catchfinally块都定义了返回语句会发生什么?

在上一节中,你看到即使catch块定义了return语句,finally块也会执行。对于一个定义了try-catch-finally块的方法,如果catchfinally都返回一个值,调用方法会返回什么?

这里有一个例子:

上述代码的输出是

20

如果catchfinally块都定义了return语句,调用方法将从finally块接收一个值。

7.4.7. 如果finally块修改了从catch块返回的值会发生什么?

如果catch块返回一个原始数据类型,finally块不能修改它返回的值。这里有一个例子:

上述代码的输出如下:

About to return :10
Return value is now :20
In Main:10

即使finally块给变量returnVal增加了10,这个修改后的值也不会返回到main方法。在执行finally块之前,catch块中的控制流会将returnVal的值复制并返回,所以当finally执行时,返回的值不会被修改。

如果方法返回一个对象,上述代码会以类似的方式行为吗?看看你自己:

这是上述代码的输出:

About to return :10
Return value is now :1010
In Main:1010

在这种情况下,catch 块返回 StringBuilder 类的对象。当 finally 块执行时,它可以访问由变量 returnVal 指向的对象的值,并且可以修改它。修改后的值返回到 main 方法。记住,原始数据类型是按值传递的,而对象是按引用传递的。

考试提示

注意从 catch 块返回值并在 finally 块中修改它的代码。如果一个 catch 块返回一个原始数据类型,finally 块不能修改它返回的值。如果一个 catch 块返回一个对象,finally 块可以修改它返回的对象的状态。

7.4.8. 一个 try 块是否只能跟随一个 finally 块?

语法上,你可以定义一个可能只跟随一个 finally 块的 try 块:

class NoCatchOnlyFinally {
    public static void main(String args[]) {
        String name = null;
        try {
            System.out.println("Try block : open resource 1");
            System.out.println("Try block : open resource 2");

            System.out.println("in try : " + name.length());
            System.out.println("Try block : close resources");
        } finally {
            System.out.println("finally : close resources");
        }
    }
}

以下是前面代码的输出:

Try block : open resource 1
Try block : open resource 2
finally : close resources
Exception in thread "main" java.lang.NullPointerException
    at NoCatchOnlyFinally.main(NoCatchOnlyFinally.java:7)

因为前面代码中的 main() 抛出了一个未检查的异常 NullPointer-Exception,所以它编译成功。但如果 try 块中声明的代码抛出一个检查异常,则它必须后面跟着一个 catch 块,或者定义它的方法必须声明抛出它。

7.4.9. 在 catch 块中捕获的异常的顺序重要吗?

对于无关的类,顺序并不重要。但对于具有 IS-A 关系的关联类,顺序很重要,因为 catch 块是从上到下检查以找到处理给定异常的合适块。

在后一种情况下,如果你在派生类的异常之前尝试捕获基类的异常,你的代码将无法编译。这种行为可能看起来很奇怪,但有一个合理的理由。正如你所知,派生类的对象可以被分配给基类的变量。同样,如果你在派生类之前尝试捕获基类的异常,派生类的异常处理程序将永远无法到达,因此代码将无法编译。

检查 图 7.15 中的代码,它已经通过在 FileNotFoundExceptioncatch 块之前定义 IOExceptioncatch 块而进行了修改。

图 7.15. 异常处理程序放置的顺序很重要。

图 7.16 展示了一种有趣的方式来记住顺序的重要性。正如你所知,抛出的异常会寻找合适的异常处理程序,从第一个处理程序开始,一直到最后一个。让我们将抛出的异常比作老虎,将异常处理程序比作允许某些类型的生物进入的门。就像抛出的异常一样,老虎应该从第一个门开始,然后继续到其余的门,直到找到匹配项。

图 7.16. 一种视觉方式来记住在 catch 块中捕获的异常的顺序很重要

老虎从第一个门开始,允许所有动物进入。哇!老虎进入了第一个门,永远不会到达专为老虎准备的第二个门。在 Java 中,当出现这种情况时,Java 编译器会拒绝编译代码,因为后续的异常处理代码将永远不会执行。如果代码中包含不可达的语句,Java 不会编译代码。

需要记住的规则

这里有一些规则,你需要回答 OCA Java SE 8 程序员 I 考试中的问题:

  • 一个try块可以后面跟着多个catch块,而catch块后面可以跟着一个单独的finally块。

  • 一个try块可以后面跟着一个catch块或一个finally块,或者两者都有。但如果try块中的代码抛出一个受检异常,单独的finally块是不够的。在这种情况下,你需要捕获受检异常或声明你的方法会抛出它。否则,你的代码无法编译。

  • trycatchfinally块不能独立存在。

  • finally块不能出现在catch块之前。

  • finally块总是执行,无论代码是否抛出异常。

7.4.10. 我能否重新抛出一个异常或我捕获的错误?

你可以对异常做任何你想做的事情。重新抛出它,将其传递给一个方法,将其赋值给另一个变量,上传到服务器,通过短信发送,等等。检查以下代码:

图片描述

哎呀!之前的代码无法编译,你得到了以下编译错误信息:

ReThrowException.java:9: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown
            throw fnfe;
            ^

当你重新抛出一个受检异常时,它会被当作一个常规的抛出的受检异常来处理,这意味着处理受检异常的所有规则都适用于它。在先前的例子中,代码既没有捕获重新抛出的FileNotFoundException异常,也没有使用throw子句声明方法myMethod会抛出它。因此,代码无法编译。

以下(修改后的)代码声明方法myMethod会抛出FileNotFoundException,并且可以成功编译:

图片描述

另一个需要注意的有趣点是,之前的代码不适用于RuntimeException。你可以重新抛出一个运行时异常,但不需要捕获它,也不需要修改你的方法签名以包含throws子句。这个规则简单的原因是RuntimeException不是受检异常,它们可能不会被你的代码捕获或声明为抛出(异常类别在第 7.2 节中详细讨论)。

7.4.11. 我能否声明我的方法抛出一个受检异常而不是处理它?

如果一个方法不希望处理它调用的方法抛出的受检异常,它可以在自己的方法声明中使用throws子句来声明抛出这些异常。考察以下示例,其中方法myMethod没有包含异常处理器;相反,它使用声明中的throws子句重新抛出由FileInputStream类的构造函数抛出的IOException

任何调用myMethod的方法现在必须要么捕获异常IOException,要么在其方法签名中声明它将被重新抛出。

7.4.12. 我可以创建嵌套循环,那么我也能创建嵌套的try-catch块吗?

简单的答案是肯定的,你可以在另一个try-catch-finally块内定义一个try-catch-finally块。理论上,try-catch-finally块的嵌套级别没有限制。

在以下示例中,在外部try块的tryfinally块中定义了另一组try-catch块:

现在来一个 Twist in the Tale 练习,这将测试你对嵌套try-catch块抛出和捕获的异常的理解。在这个练习中,内部try块定义了抛出NullPointerException的代码。但内部try块没有为这个异常定义异常处理器。外部try块会捕获这个异常吗?亲自看看(答案见附录)。

Twist in the Tale 7.2

假设你的系统上存在players.txt文件,并且如粗体所示,players的分配不会抛出任何异常,那么以下代码的输出是什么?

import java.io.*;
public class TwistInTaleNestedTryCatch {
    static FileInputStream players, coach;
    public static void main(String args[]) {
        try {
            players = new FileInputStream("players.txt");
            System.out.println("players.txt found");
            try {
                coach.close();
            } catch (IOException e) {
                System.out.println("coach.txt not found");
            }
        } catch (FileNotFoundException fnfe) {
            System.out.println("players.txt not found");
        } catch (NullPointerException ne) {
            System.out.println("NullPointerException");
        }
    }
}
  1. 代码打印

    players.txt found
    NullPointerException
    
  2. 代码打印

    players.txt found
    coach.txt not found
    
  3. 代码抛出一个运行时异常。

  4. 代码无法编译。

7.4.13. 我应该处理错误吗?

虽然你可以定义代码来处理错误,但你不应该这样做。相反,你应该让 JVM 处理错误。以下示例显示了如何捕获错误:

虽然你不应该在代码中处理错误,但如果你这样做会发生什么?处理代码的异常处理器会执行吗?通过回答以下故事中的 Twist in the Tale 练习来亲自看看(答案见附录)。

Twist in the Tale 7.3

错误处理块中的代码会执行吗?你认为以下代码的输出是什么?

public class TwistInTaleCatchError {
    public static void main(String args[]) {
        try {
            myMethod();
        } catch (StackOverflowError s) {
            for (int i=0; i<2; ++i)
                System.out.println(i);
        }
    }
    public static void myMethod() {
        myMethod();
    }
}
  1. 0
    
  2. java.lang.StackOverFlowError
    
  3. 0
    1
    
  4. 0
    1
    2
    java.lang.StackOverFlowError
    

在下一节中,你将处理考试中的特定异常类和错误。

7.5. 常见异常类和类别

[8.5] “识别常见异常类(如 NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException、ClassCastException)”

在本节中,我们将查看常见的异常类和异常类别。你还将了解这些异常被抛出的场景以及如何处理它们。

对于这次考试,您应该熟悉导致这些常见异常类和类别的情况以及如何处理它们。表 7.2 列出了常见错误和异常。尽管考试具体列出了四个运行时异常,但在考试中您可能会看到其他常见的异常和错误类。

表 7.2. 常见错误和异常
运行时异常 错误
ArrayIndexOutOfBoundsException ExceptionInInitializerError
IndexOutOfBoundsException StackOverflowError
ClassCastException NoClassDefFoundError
IllegalArgumentException OutOfMemoryError
ArithmeticException
NullPointerException
NumberFormatException

OCA Java SE 8 程序员 I 级考试目标要求您理解之前提到的哪些错误和异常是由 JVM 抛出的,哪些应该通过程序抛出。从本章前面关于错误的讨论中,您知道错误代表与 JRE 相关的问题,例如OutOfMemoryError。作为程序员,您不应该抛出或捕获这些错误——让 JVM 处理。运行时异常的定义指出,这些是 JVM 抛出的异常,您不应该在程序中抛出。

让我们逐一详细回顾这些内容。

7.5.1. ArrayIndexOutOfBoundsExceptionIndexOutOfBoundsException

如图 7.17 所示,ArrayIndexOutOfBoundsExceptionIndexOutOfBounds-Exception是运行时异常,它们之间存在 IS-A 关系。IndexOutOfBoundsExceptionArrayIndexOutOfBoundsException的子类。

图 7.17ArrayIndexOutOfBoundsException的类层次结构

图片

当一段代码尝试访问超出其边界的数组(数组被访问的位置小于 0 或大于或等于其长度)时,将抛出ArrayIndexOutOfBoundsException异常。当一段代码尝试使用非法索引访问列表,如ArrayList时,将抛出IndexOutOfBoundsException异常。

假设已经定义了一个数组和列表,如下所示:

String[] season = {"Spring", "Summer"};
ArrayList<String> exams = new ArrayList<>();
exams.add("SCJP");
exams.add("SCWCD");

以下代码行将抛出ArrayIndexOutOfBoundsException异常:

图片

以下代码行将抛出IndexOutOfBoundsException异常:

图片

您认为 JVM 为什么承担了抛出这个异常的责任?其中一个主要原因是这个异常直到运行时才知道,并且依赖于代码访问的数组或列表位置。通常,一个变量用于指定这个数组或列表位置,其值可能直到运行时才知道。

注意

当尝试访问无效的数组位置时,将抛出ArrayIndexOutOfBoundsException异常。当尝试使用非法索引访问ArrayList等列表时,将抛出IndexOutOfBoundsException异常。

如果你检查你要尝试访问的索引位置是否大于或等于 0 并且小于你的数组或 ArrayList 的大小,你可以避免抛出这些异常。

7.5.2. ClassCastException

在我开始讨论这个异常的例子之前,快速看一下 图 7.18 以复习这个异常的类层次结构。

图 7.18. ClassCastException 的类层次结构

图片

检查下一列表中的代码,其中抛出 ClassCastException 的代码行以粗体显示。

列表 7.2. 抛出 ClassCastException 的代码示例

图片

当一个对象在类型转换时失败 IS-A 测试时,会抛出 ClassCastException。在先前的例子中,类 InkColorInkBlackInk 类的基类。在先前的案例中,JVM 抛出 ClassCastException,因为粗体部分的代码试图显式地将 ColorInk 类型的对象转换为 BlackInk 类型。

注意,此代码行避免了编译错误,因为变量 inks 定义了一个类型为 InkArrayList,它可以存储 Ink 类型的对象及其所有子类的对象。然后代码正确地添加了允许的对象:一个 BlackInk 和一个 ColorInk。如果代码定义了一个类型为 BlackInkColorInkArrayList,则代码将无法编译,如下所示:

图片

这是之前修改过的代码块抛出的编译错误:

Invalid.java:6: inconvertible types
found   : ColorInk
required: BlackInk
        Ink ink = (BlackInk)inks.get(0);
                                    ^

你可以在类型转换之前使用 instanceof 操作符来验证一个对象是否可以被转换为另一个类。假设 InkColorInkBlackInk 类的定义与先前的例子中相同,以下代码行将避免 ClassCastException

图片

在前面的例子中,条件 (inks.get(0)instanceofBlackInk) 评估为 false,因此 if 语句的 then 部分不会执行。

在接下来的 Twist in the Tale 练习中,我将介绍在 列表 7.2 中的类型转换例子中使用的接口(答案见附录)。

故事转折 7.4

让我们介绍一个在 列表 7.2 中使用的接口,并看看它的行为。以下是修改后的代码。检查代码并选择正确的选项:

class Ink{}
interface Printable {}
class ColorInk extends Ink implements Printable {}
class BlackInk extends Ink{}

class TwistInTaleCasting {
    public static void main(String args[]) {
        Printable printable = null;
        BlackInk blackInk = new BlackInk();
        printable = (Printable)blackInk;
    }
}
  1. printable = (Printable)blackInk 将会抛出编译错误

  2. printable = (Printable)blackInk 将会抛出运行时异常

  3. printable = (Printable)blackInk 将会抛出检查异常

  4. 以下代码行将无法编译:

    printable = blackInk;
    

7.5.3. IllegalArgumentException

如同这个异常的名称所暗示的,IllegalArgumentException 被抛出以指定一个方法传递了非法或不适当的参数。其类层次结构如图 7.19 所示。

图 7.19. IllegalArgumentException 的类层次结构

图片

尽管这是一个运行时异常,但程序员通常使用这个异常来验证传递给方法的参数。异常构造函数传递了一个描述性的消息,指定了异常的详细信息。请检查以下代码:

public void login(String username, String pwd, int maxLoginAttempt) {
    if (username == null || username.length() < 6)
        throw new IllegalArgumentException
                   ("Login:username can't be shorter than 6 chars");
    if (pwd == null || pwd.length() < 8)
        throw new IllegalArgumentException
                   ("Login: pwd cannot be shorter than 8 chars");
    if (maxLoginAttempt < 0)
        throw new IllegalArgumentException
                   ("Login: Invalid loginattempt val");

    //.. rest of the method code
}

之前的方法验证了传递给它的各种方法参数,如果它们不符合方法的要求,则抛出适当的 IllegalArgumentExceptionIllegalArgumentException 的每个对象都传递了一个不同的 String 消息,简要描述了它。

7.5.4. NullPointerException

如 图 7.20 所示的 NullPointerException 是一个典型的异常。

图 7.20. NullPointerException 的类层次结构

我想象几乎所有 Java 程序员都尝过这个异常的滋味,但让我们来看看它的解释。

如果您尝试通过 null 值访问非静态方法或变量,JVM 会抛出这个异常。考试可能会有有趣的代码组合来测试您是否会在特定的代码片段中抛出 NullPointerException。关键是确保引用变量已被分配了一个非 null 值。特别是,我将讨论以下情况:

  • 访问显式分配了 null 值的引用变量的成员

  • 使用未初始化的局部变量,这可能会 看似 抛出 NullPointerException

  • 尝试访问不存在的数组位置

  • 使用分配了 null 值的数组元素的成员

让我们从第一种情况开始,其中变量被显式地分配了一个 null 值:

之前的代码尝试访问变量 list 上的 add 方法,而该变量已被分配了一个 null 值。这会抛出一个异常,如下所示:

Exception in thread "main" java.lang.NullPointerException
    at ThrowNullPointerException.main(ThrowNullPointerException.java:5)

默认情况下,类的 static 和实例变量被分配一个 null 值。在之前的例子中,static 变量 list 被显式地分配了一个 null 值。为了帮助您澄清代码并避免任何可能的疑问,list 被显式地分配了一个 null 值。当 main 方法尝试在变量 list 上执行 add 方法时,它会在一个 null 值上调用一个方法。这个调用会导致 JVM 抛出一个 Null-Pointer-Exception(这是一个 RuntimeException)。如果您将变量 list 定义为一个实例变量并且没有给它分配一个显式值,您将得到相同的结果(在运行时抛出 NullPointerException)。因为 static 方法 main 无法访问实例变量 list,您需要创建 ThrowNullPointerException 类的一个对象来访问它:

您可以通过在尝试访问对象的成员之前检查该对象是否为 null 来防止抛出 NullPointerException

如果您将之前的代码修改如下,会发生什么?它还会抛出 NullPointerException 吗?

有趣的是,之前的代码无法编译。list是在main方法内部定义的局部变量,并且默认情况下局部变量没有赋予值——甚至没有null值。如果你尝试使用未初始化的局部变量,你的代码将无法编译。在考试中注意类似的问题。

当代码可能抛出NullPointerException的另一组条件涉及数组的使用:

图片

在前面的代码中,静态变量oldLaptops默认赋予null值。它的数组元素既未初始化也未赋予值。尝试访问数组第二个元素的代码将抛出NullPointerException

在以下代码中,变量newLaptops的两个数组元素被初始化并赋予默认值null。如果你在变量newLaptops的第二元素上调用toString方法,它将导致抛出NullPointerException

图片

如果你将代码在图片处进行修改如下,它不会抛出异常——它将打印值null。这是因为基于对象的System.out.println()重载调用了基于对象的String.valueOf()重载,它本身会检查要“打印”的对象是否为null,如果是,则输出null而不会调用任何toString()方法:

图片

考试技巧

在考试中,请注意那些尝试使用未初始化局部变量的代码。因为这样的变量甚至没有初始化为null值,所以你不能使用System.out.println方法打印它们的值。这样的代码无法编译。

让我们修改之前使用变量oldLaptops的代码,并检查你对NullPointerException的理解。这里有一个故事转折的动手练习供你完成(答案见附录)。

故事转折 7.5

让我们检查你对NullPointerException的理解。这里有一个代码片段。检查代码并选择正确的答案。

class TwistInTaleNullPointerException {
    public static void main(String[] args) {
        String[][] oldLaptops =

             { {"Dell", "Toshiba", "Vaio"}, null,
{"IBM"}, new String[10] };
        System.out.println(oldLaptops[0][0]);          // line 1
        System.out.println(oldLaptops[1]);             // line 2
        System.out.println(oldLaptops[3][6]);          // line 3
        System.out.println(oldLaptops[3][0].length()); // line 4
        System.out.println(oldLaptops);                // line 5
    }
}
  1. 第 1 行的代码将抛出NullPointerException

  2. 第 1 行和第 3 行的代码将抛出NullPointerException

  3. 只有第 4 行的代码将抛出NullPointerException

  4. 第 3 行和第 5 行的代码将抛出NullPointerException

7.5.5. ArithmeticException

当 JVM 遇到异常数学条件,例如整除以零时,它将抛出ArithmeticException(如图 7.21 所示,展示了类层次结构)。请注意,除以 0 与除以 0.0 不同。在本节中,我们将介绍整数和十进制数除以 0 和 0.0 的结果。

图 7.21。ArithmeticException的类层次结构

图片

以下总结了ArithmeticException的原因:

  • 只要只涉及整数,除法将作为整数除法执行。一旦有浮点数,那么所有计算都将在浮点算术中进行(实际上对所有算术运算都适用)。

  • 整数除以零会抛出ArithmeticException

  • 浮点除以零不会抛出任何异常,而是返回±InfinityNaN,具体取决于第一个操作数。

整数除以 0

虽然这个异常的发生可能看起来很简单,但假设可能是错误的。让我们从一个简单且明确的例子开始(这个例子很容易发现):

图片

在执行时,前面的代码将抛出一个具有类似消息的ArithmeticException

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at ThrowArithmeticEx.main(ThrowArithmeticEx.java:3)

这里是一个可能在考试中看到的相对复杂的代码示例。你认为它会抛出ArithmeticException吗?你也认为答案像前面的代码一样明显吗?

class ThrowArithmeticEx {
    public static void main(String args[]) {
        int a = 10;
        int y = a++;
        int z = y--;

        int x1 = a - 2*y - z;
        int x2 = a - 11;
        int x = x1/ x2;

        System.out.println(x);
    }
}

The preceding code throws ArithmeticException for the operation x1/x2 because the value of x2 is 0. With the initialization of the variable y, the value of variable a is incremented by 1, from 10 to 11 (due to the post-fix increment operator). The variable x2 is initialized with a value that’s equal to 11 and less than a, which is 0.

考试技巧

在考试中,要注意整数的除法。如果除数是 0,被除的整数值无关紧要。此类操作将抛出ArithmeticException

你认为除以 0 的答案会是什么?你认为以下代码的输出是10,还是ArithmeticException

class ThrowArithmeticEx {
    public static void main(String args[]) {
        int x = (int)(7.3/10.6);
        int y = (int)(100.76/123.87);

        int z = x/y;

        System.out.println(x);
    }
}

整数除以 0 将导致ArithmeticException。因此,前面的代码也将抛出ArithmeticException

考试技巧

负数或正整数除以 0 的结果将是ArithmeticException

以下是一个除以 0 的显式示例:

图片

考试技巧

除以 0 的结果是ArithmeticException

小数除以 0

如果你将正小数除以 0,答案是Infinity

图片

如果你将负小数除以 0,答案是-Infinity

图片

考试技巧

如果你将正小数除以 0,结果是Infinity。如果你将负小数除以 0,结果是-Infinity

这里有一个有趣的问题:你认为 0.0 除以 0 的结果会是什么?这里有一个简短的代码片段:

图片

考试技巧

0.0 除以 0 的结果是NaN(不是一个数字)。

任何包含NaN的数学运算结果都是NaN

整数或小数除以 0.0

除以 0 和除以 0.0 的结果并不相同。让我们回顾前面的例子,从本节第一个例子的修改版开始:

图片

前面的代码不会抛出ArithmeticException。它输出Infinity

考试技巧

当正整数或小数除以 0.0 时,结果是Infinity

这里是另一个修改后的例子:

class DivideByZeroPointZero {
    public static void main(String args[]) {
        int a = 10;
        int y = a++;
        int z = y--;

        int x1 = a - 2*y - z;
        int x2 = a - 11;
        double x3 = x2;

        double x = x1/ x3;

        System.out.println(x);
        System.out.println(x1);
        System.out.println(x3);
    }
}

以下是前面代码的输出:

-Infinity
-17
0.0

前面的代码不会抛出 ArithmeticException。变量 x1 被分配了一个负整数值,即 -17。变量 x2 被分配了值 0。当 double 类型的变量 x3 使用 x2 的值初始化时,它被提升为 double 值,将 0.0 分配给 x3。当一个负整数值除以 0.0 时,结果是 –Infinity

考试技巧

当一个负整数或小数除以 0.0 时,结果是 –Infinity

7.5.6. 数值格式异常

如果你尝试将“87”和“9m#”转换为数值,会发生什么?前者值是正确的,但你无法将后者值转换为数值,除非它是一个编码值,直接来自一部詹姆斯·邦德电影,可以转换为任何东西。

如 图 7.22 所示,NumberFormat-Exception 是一个运行时异常。它被抛出以指示应用程序尝试将一个字符串(具有不适当的格式)转换为数值类型之一。

图 7.22. NumberFormatException 的类层次结构

![07fig22.jpg]

Java API 中的多个类定义了解析方法。最常用的方法之一是来自 Integer 类的 parseInt 方法。它用于将 String 参数解析为有符号(负数或正数)的十进制整数。以下是一些示例:

图片

从 Java 7 开始,你可以在数值字面量中使用下划线(_)。但是,你无法在传递给 parseInt 方法的 String 值中使用它们。字母 ABCD 在十进制数制中不使用,但在十六进制数制中可以使用,因此你可以通过指定数制的基数为 16 将十六进制字面量值 "12ABCD" 转换为十进制数制:

图片

注意,参数 16 是传递给 parseInt 方法,而不是传递给 println 方法。以下将无法编译:

图片

你可以从自己的方法中抛出 NumberFormatException 来指示 String 值转换为指定数值格式(十进制、八进制、十六进制、二进制)存在问题,并且你可以添加自定义异常消息。这种异常最常见的候选方法是用于将命令行参数(作为 String 值接受)转换为数值的方法。请注意,所有命令行参数都作为 String 数组以 String 值的形式接受。

以下是一个程序抛出 NumberFormatException 的示例:

图片

十六进制字面量 16b 转换为十进制数制是成功的。但是,十六进制字面量 65v 转换为十进制数制失败,并且前面的代码将给出以下输出:

363
Exception in thread "main" java.lang.NumberFormatException: 65v cannot be converted to hexadecimal number
    at ThrowNumberFormatException.convertToNum(ThrowNumberFormatException.java:8)
    at ThrowNumberFormatException.main(ThrowNumberFormatException.java:14)

现在我们来看一下这次考试中涵盖的一些常见错误。

7.5.7. 初始化异常

ExceptionInInitializerError错误通常由 JVM 在代码中的静态初始化块抛出任何类型的RuntimeException时抛出。图 7.23 显示了ExceptionInInitializer-Error的类层次结构。

图 7.23. ExceptionInInitializerError的类层次结构

在类中定义一个static初始化块,使用关键字static,后跟大括号。此块在类中定义,但不在方法中定义。它通常用于在类首次加载时执行代码。以下任何情况引发的运行时异常都会抛出此错误:

  • 执行匿名static

  • 初始化static变量

  • 执行一个static方法(从前面两个项目中的任何一个调用)

在以下示例中定义的类的static初始化块将抛出NumberFormatException,当 JVM 尝试加载这个类时,它将抛出ExceptionInInitializerError

public class DemoExceptionInInitializerError {
    static {
        int num = Integer.parseInt("sd", 16);
    }
}

当 JVM 尝试加载类DemoExceptionInInitializerError时,以下是其错误信息:

java.lang.ExceptionInInitializerError
Caused by: java.lang.NumberFormatException: For input string: "sd"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
    at java.lang.Integer.parseInt(Integer.java:447)
    at DemoExceptionInInitializerError.<clinit>(DemoExceptionInInitializerError.java:3)
考试提示

在 OCA Java SE 8 程序员 I 考试中,注意看似简单的代码。前面提到的DemoExceptionInInitializerError类看似简单,但它是一个很好的考试问题候选者。正如你所知,当 JVM 尝试加载这个类时,它会抛出ExceptionInInitializerError错误。

在以下示例中,静态变量的初始化导致抛出NullPointerException。当 JVM 加载这个类时,它将抛出ExceptionInInitializerError

public class DemoExceptionInInitializerError1 {
    static String name = null;
    static int nameLength = name.length();
}

当 JVM 尝试加载DemoException-InInitializer-Error1类时,错误信息如下:

java.lang.ExceptionInInitializerError
Caused by: java.lang.NullPointerException
    at DemoExceptionInInitializerError1.<clinit>(DemoExceptionInInitializerError1.java:3)
Exception in thread "main"

现在让我们继续讨论由static方法抛出的异常,这些方法可能由static初始化块调用或用于初始化static变量。检查以下代码,其中MyException是一个用户定义的RuntimeException

这是DemoExceptionInInitializerError2类抛出的错误:

java.lang.ExceptionInInitializerError
Caused by: MyException
    at DemoExceptionInInitializerError2.getName(DemoExceptionInInitializerError2.java:4)
    at DemoExceptionInInitializerError2.<clinit>(DemoExceptionInInitializerError2.java:2)

你注意到错误ExceptionInInitializerError只能由运行时异常引起吗?这当然有合理的理由。

如果静态初始化块抛出错误,它不会从错误中恢复并返回代码以抛出ExceptionInInitializerError异常。如果静态初始化块抛出一个受检异常的对象,则无法抛出此错误,因为 Java 编译器足够智能,可以确定这种条件,并且不允许你在静态初始化块中抛出未处理的受检异常。

考试提示

ExceptionInInitializerError只能由RuntimeException的对象引起。它不能作为static初始化块抛出的错误或受检异常的结果发生。

7.5.8. StackOverflowError

StackOverflowError 错误扩展了 Virtual-MachineError(如图 7.24 所示)。正如其名称所暗示的,你应该将其留给 JVM 管理。

图 7.24. StackOverflowError 的类层次结构

当一个 Java 程序调用自身多次,以至于执行 Java 程序分配的内存栈“溢出”时(“溢出”意味着栈超过了某个大小),JVM 会抛出此错误。检查以下代码,其中方法递归调用自身而没有退出条件:

以下代码抛出了以下错误:

Exception in thread "main" java.lang.StackOverflowError
    at DemoStackOverflowError.recursion(DemoStackOverflowError.java:3)

7.5.9. NoClassDefFoundError

如果你未能设置你的类路径,结果 JVM 无法加载你想要访问或执行的类,会发生什么?或者,如果你在编译应用程序之前尝试运行它(因此找不到你试图使用的类的 .class 文件),会发生什么?在这两种情况下,JVM 将抛出 NoClassDefFoundError(如图 7.25 所示的类层次结构)。

图 7.25. NoClassDefFoundError 的类层次结构

这是 Java API 文档关于此错误的说明:

如果 Java 虚拟机或 ClassLoader 实例尝试加载类的定义(作为正常方法调用的一部分或作为使用 new 表达式创建新实例的一部分),并且找不到类的定义,则会抛出异常。1

¹

NoClassDefFoundError 的文档可以在 Javadoc 中找到:docs.oracle.com/javase/8/docs/api/java/lang/NoClassDefFoundError.html

由于这个特定的错误不是编码问题,我没有为你提供一个编码示例。正如你可以从图 7.25 中的错误层次结构图中看到的,这是一个在运行时由于缺少类文件定义而产生的链接错误。像每个系统错误一样,这个错误不应该由代码处理,而应该完全由 JVM 处理。

注意

不要混淆用于加载类的 Class.forName() 抛出的异常和 JVM 抛出的 NoClassDefFoundErrorClass.forName() 抛出 ClassNotFoundException

7.5.10. OutOfMemoryError

如果你在应用程序中创建并使用了大量的对象——例如,如果你加载了大量持久数据供应用程序处理,会发生什么?在这种情况下,JVM 可能会在堆上耗尽内存,垃圾收集器可能无法为 JVM 释放更多内存。在这种情况下,JVM 无法在堆上创建更多对象。将抛出 OutOfMemoryError(如图 7.26 所示的类层次结构)。

图 7.26. OutOfMemoryError 的类层次结构

无论您在哪个平台上工作,您都将始终与有限的堆大小一起工作,因此您不能在应用程序中创建和使用无限数量的对象。为了解决这个问题,您需要限制应用程序创建的资源或对象数量,或者增加您正在工作的平台上的堆大小。

有许多工具(这些工具超出了本书的范围)可以帮助您监控应用程序中创建的对象数量。

7.6. 摘要

在本章中,我们讨论了异常处理的需求,以及将异常处理代码与程序逻辑分开定义的优势。您看到了这种方法如何帮助分离定义常规程序逻辑和异常处理代码的关心点。我们还探讨了实现异常处理代码的代码语法,特别是try-catch-finally块。抛出异常的代码应包含在try块中,该块紧随catch和/或finally块之后。try块可以后跟多个catch块,但只能有一个finally块。finally块不能放在try块之前。try块必须后跟至少一个catchfinally块。trycatchfinally块不能独立存在。

接下来,我们深入探讨了异常的不同类别:检查异常、运行时或非检查异常、错误。检查异常是java.lang.Exception类的子类。非检查异常是java.lang.RuntimeException类的子类,而java.lang.RuntimeException本身又是java.lang.Exception类的子类。错误是java.lang.Error类的子类。所有这些异常都是java.lang.Throwable类的子类。

检查异常是方法作者预见但不在代码直接控制范围内的不可接受条件。运行时异常代表编程错误——这些错误是由于对其他代码的不当使用而发生的。错误是严重的异常,由 JVM 抛出,是由于处理您的代码的环境状态错误导致的。

在本章的最后几节中,我们讨论了常见异常和错误,例如NullPointerExceptionIllegalArgumentExceptionStackOverflowError等。对于这些错误和异常,我解释了它们在代码中可能抛出的条件以及是否应该在异常处理器中显式处理。

7.7. 复习笔记

本节列出了本章涵盖的所有部分的要点。

为什么需要单独处理异常:

  • 将异常单独处理使您能够一起定义代码的主要逻辑。

  • 如果不使用单独的异常处理器,您的代码的主要逻辑将丢失在应对异常条件的过程中。(参见图 7.5 以获取示例。)

  • 异常处理器将定义常规程序逻辑与异常处理代码的关注点分开。

  • 异常通过提供异常或错误的堆栈跟踪,帮助确定有问题的代码以及定义它的方法。

  • JVM 可能会将未处理的异常的堆栈跟踪发送到 Java 控制台。

异常的分类:

  • 异常分为三类:检查型异常、运行时(或非检查型)异常和错误。这三个类别共享 IS-A 关系(继承)。

  • java.lang.RuntimeException类的子类被归类为运行时异常。

  • java.lang.Error类的子类被归类为错误。

  • 如果类的子类不是java.lang.RuntimeException的子类,则java.lang.Exception的子类被归类为检查型异常。

  • java.lang.RuntimeException类是java.lang.Exception类的子类。

  • java.lang.Exception类是java.lang.Throwable类的子类。

  • java.lang.Error类也是java.lang.Throwable类的子类。

  • java.lang.Throwable类继承自java.lang.Object类。

检查型异常:

  • 检查型异常是方法作者预见但不在代码直接控制范围内的不可接受条件。

  • 检查型异常是java.lang.Exception类的子类,但不是java.lang.RuntimeException的子类。然而,值得注意的是,java.lang.RuntimeException类本身也是java.lang.Exception类的子类。

  • 如果一个方法调用另一个可能抛出检查型异常的方法,那么它必须被包含在一个try-catch块中,或者该方法应在方法签名中声明抛出此异常。

运行时异常:

  • 运行时异常代表编程错误。这些错误通常是由于对其他代码的不当使用而发生的。例如,NullPointerException是一个运行时异常,当一段代码尝试在一个尚未分配对象的变量上执行代码并指向null时发生。另一个例子是ArrayIndexOutOfBoundsException,当一段代码尝试访问一个列表元素在不存在位置上的数组时抛出。

  • 运行时异常是java.lang.RuntimeException的子类。

  • 即使一个方法可能抛出运行时异常,运行时异常也可能不是方法签名的一部分。

  • 运行时异常不一定会被try-catch块捕获。

错误:

  • 错误是一个严重的异常,由 JVM 在环境状态错误时抛出,该错误处理你的代码。例如,NoClassDefFoundError是 JVM 在无法找到它应该运行的.class文件时抛出的错误。

  • StackOverflowError是另一种错误,当 Java 程序栈所需的内存大小超过 JRE 为 Java 应用程序提供的内存时,JVM 会抛出此错误。此错误通常是由于无限循环或高度嵌套的循环引起的。

  • 错误是java.lang.Error类的一个子类。

  • 错误不必是方法签名的一部分。

  • 虽然你可以从语法上处理错误,但当这些错误发生时,你能做的很少。通常,普通程序不被期望从错误中恢复。

创建抛出异常的方法:

  • 方法使用throw语句抛出异常或错误。

  • 方法使用其签名中的throws子句来声明它可能会抛出异常。

  • 方法可以在其throws子句中有多个以逗号分隔的异常类名。在方法声明中包括运行时异常或错误不是必需的。

  • 语法上,你不必总是需要一个throwthrows语句的组合来创建一个抛出异常的方法(受检或非受检)。你可以用一个抛出异常的方法来替换throw语句。

  • 要使用抛出受检异常的方法,你必须执行以下操作之一:

    • 处理异常——将代码放在try块中并捕获抛出的异常。

    • 声明抛出——使用throws子句声明要抛出的异常。

    • 处理和声明——同时实现上述两种选项。

  • 在创建抛出运行时异常或错误的函数时,包括异常或错误名称在throws子句中不是必需的。

  • 抛出运行时异常或错误的函数不受处理或声明规则的约束。

  • 一种方法可以声明抛出所有类型的异常,即使它实际上并不抛出。但是,如果try块没有抛出该受检异常或使用声明抛出该受检异常的方法,则不能为该受检异常(除了Exception)定义一个catch块。

抛出异常时会发生什么:

  • 异常是java.lang.Throwable类的一个对象。

  • 当一段代码遇到以异常条件形式出现的障碍时,它会创建一个java.lang.Throwable子类的对象,用必要的信息(例如其类型和可选的文本描述以及有问题的程序状态)初始化它,并将其交给 JVM。

  • 将可能抛出异常的代码放在try块中。

  • 定义catch块以包含在出现异常条件时执行的替代代码。

  • try块可以后跟一个或多个catch块。

  • catch块之后必须跟零个或一个finally块。

  • finally块无论try块中的代码是否抛出异常都会执行。

  • catch 块的放置顺序很重要。如果捕获的异常之间存在继承关系,则不能在派生类异常之前捕获基类异常。尝试这样做会导致编译失败。

  • 即使一个 trycatch 块定义了 return 语句,finally 块也会执行。

  • 如果 catchfinally 块都定义了 return 语句,调用方法将接收到 finally 块的值。

  • 如果 catch 块返回一个原始数据类型,finally 块不能修改它返回的值。

  • 如果 catch 块返回一个对象,finally 块可以修改它返回的值。

  • 如果 try 块中的代码抛出检查型异常,单独的 finally 块不足以与 try 块一起使用。在这种情况下,你需要捕获检查型异常或在方法签名中定义异常被抛出,否则你的代码将无法编译。

  • trycatchfinally 块不能独立存在。

  • finally 块不能出现在 catch 块之前。

  • 你可以在异常处理程序中重新抛出你捕获的错误。

  • 你可以处理异常或声明你的方法抛出异常。在后一种情况下,你不需要在代码中处理异常。这适用于检查型异常。

  • 你可以创建嵌套的异常处理程序。

  • 一个 trycatchfinally 块可以定义另一个 try-catch-finally 块。理论上,try-catch-finally 块的嵌套级别没有限制。

常见异常、类别和类:

  • 在典型的编程条件下,不应通过编程方式抛出 ArrayIndexOutOfBoundsException

  • JVM 负责抛出此异常的主要原因是此异常直到运行时才知道,并且依赖于代码访问的数组或列表位置。通常,一个变量用于指定此数组或列表位置,其值可能直到运行时才知道。

  • ClassCastException 是一个运行时异常。java.lang.ClassCastException 继承自 java.lang.RuntimeException

  • 当一个对象在将其转换为其他类类型时失败 IS-A 测试时,会抛出 ClassCastException

  • 在进行转换之前,你可以使用 instanceof 操作符来验证一个对象是否可以被转换为另一个类。

  • IllegalArgumentException 是一个运行时异常。java.lang.Illegal-Argument-Exception 继承自 java.lang.RuntimeException

  • 抛出 IllegalArgumentException 以指定方法已被传递非法或不适当的参数。

  • 尽管 IllegalArgumentException 是一个运行时异常,但程序员通常使用此异常来验证传递给方法的参数,并且异常构造函数会传递一个描述性的消息,指定异常的详细信息。

  • 作为程序员,你可以抛出一个IllegalStateException来通知调用方法,被请求执行的方法尚未准备好开始执行,或者处于无法执行的状态。

  • NullPointerException是一个运行时异常。类java.lang.NullPointerException扩展了java.lang.RuntimeException

  • 如果你尝试访问一个未初始化的引用变量的方法或变量,JVM 会抛出NullPointerException

  • 当 JVM 遇到异常数学条件,如除以零时,它会抛出ArithmeticException

  • 在整数除法中,如果除数是 0,被除的整数值无关紧要。此类操作将抛出ArithmeticException

  • 负数或正整数除以 0 的结果将是一个Arithmetic-Exception

  • 0 除以 0 的结果是一个ArithmeticException

  • 如果你将一个正小数除以 0,结果是Infinity。如果你将一个负小数除以 0,结果是-Infinity

  • 0.0 除以 0 的结果是NaN(不是一个数字)。

  • 当一个正整数或小数除以 0.0 时,结果是Infinity

  • 当一个负整数或小数除以 0.0 时,结果是–Infinity

  • NumberFormatException是一个运行时异常。java.lang.NumberFormatException扩展了java.lang.IllegalArgumentExceptionjava.lang.IllegalArgumentException扩展了java.lang.RuntimeException

  • 你可以从自己的方法中抛出一个NumberFormatException,以指示将String值转换为指定数值格式(十进制、八进制、十六进制或二进制)时存在问题。

  • 以下任何一种情况引发的运行时异常可能会抛出ExceptionInInitializerError

    • 执行一个匿名的static

    • 初始化一个static变量

    • 执行一个static方法(从前面两个项目中的任何一个调用)

  • 只有运行时异常的对象可以抛出错误ExceptionInInitializerError

  • 如果一个static初始化块抛出一个检查异常的对象,则不能抛出ExceptionInInitializerError,因为 Java 编译器足够智能,可以确定这种条件,并且不允许你从一个static初始化块中抛出一个未处理的检查异常。

  • StackOverflowError是一个错误。java.lang.StackOverflowError扩展了java.lang.VirtualMachineError

  • 因为StackOverflowError扩展了VirtualMachineError,所以它应该由 JVM 管理。

  • 当一个 Java 程序调用自身次数过多,以至于执行 Java 程序的内存栈“溢出”时,JVM 会抛出StackOverflowError错误。

  • NoClassDefFoundError是一个Errorjava.lang.NoClassDefFoundError扩展了java.lang.LinkageErrorjava.lang.LinkageError扩展了java.lang.Error

  • 当 JVM 或 ClassLoader 无法加载创建类对象所需的类定义时,会抛出 NoClassDefFoundError

  • 不要混淆用于加载类的 Class.forName() 抛出的异常和 JVM 抛出的 NoClassDefFoundErrorClass.forName() 抛出 ClassNotFoundException

  • 当 JVM 无法在堆上创建对象且垃圾收集器可能无法为 JVM 释放更多内存时,会抛出 OutOfMemoryError

7.8. 样本考试问题

Q7-1.

以下代码的输出是什么:

class Course {
    String courseName;
    Course() {
        Course c = new Course();
        c.courseName = "Oracle";
    }
}
class EJavaGuruPrivate {
    public static void main(String args[]) {
        Course c = new Course();
        c.courseName = "Java";
        System.out.println(c.courseName);
    }
}
  1. 代码将打印 Java
  2. 代码将打印 Oracle
  3. 代码将无法编译。
  4. 代码将在运行时抛出异常或错误。

Q7-2.

选择正确的选项(s):

  1. 你无法处理运行时异常。
  2. 你不应该处理错误。
  3. 如果一个方法抛出检查异常,它必须由该方法处理或指定在其 throws 子句中。
  4. 如果一个方法抛出运行时异常,它可以在其 throws 子句中包含该异常。
  5. 运行时异常是检查异常。

Q7-3.

检查以下代码并选择正确的选项(s):

class EJavaGuruExcep {
    public static void main(String args[]) {
        EJavaGuruExcep var = new EJavaGuruExcep();
        var.printArrValues(args);
    }
    void printArrValues(String[] arr) {
        try {
            System.out.println(arr[0] + ":" + arr[1]);
        } catch (NullPointerException e) {
            System.out.println("NullPointerException");
        } catch (IndexOutOfBoundsException e) {
            System.out.println("IndexOutOfBoundsException");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("ArrayIndexOutOfBoundsException");

        }
    }
}
  1. 如果使用以下命令执行类 EJavaGuruExcep,它将打印 NullPointerException

  2. java EJavaGuruExcep
    
  3. 如果使用以下命令执行类 EJavaGuruExcep,它将打印 IndexOutOfBoundsException

  4. java EJavaGuruExcep
    
  5. 如果使用以下命令执行类 EJavaGuruExcep,它将打印 ArrayIndexOutOfBoundsException

  6. java EJavaGuruExcep one
    
  7. 代码将无法编译。

Q7-4.

以下代码的输出是什么?

class EJava {
    void method() {
        try {
            guru();
            return;
        } finally {
            System.out.println("finally 1");
        }
    }
    void guru() {
        System.out.println("guru");
        throw new StackOverflowError();
    }
    public static void main(String args[]) {
        EJava var = new EJava();
        var.method();
    }
}
  1. guru
    finally 1
    
  2. guru
    finally 1
    Exception in thread "main" java.lang.StackOverflowError
    
  3. guru
    Exception in thread "main" java.lang.StackOverflowError
    
  4. guru
    
  5. 代码无法编译。

Q7-5.

以下代码的输出是什么?

class Quest5 {
    public static void main(String args[]) {
        int arr[] = new int[5];
        arr = new int[]{1,2,3,4};

        int x = arr[1]-- + arr[0]-- /arr[0] * arr[4];
        System.out.println(x);
    }
}
  1. 代码输出一个值。
  2. 代码输出一个值后跟一个异常。
  3. ArithmeticException
  4. NullPointerException
  5. IndexOutOfBoundsException
  6. ArrayIndexOutOfBoundsException
  7. 编译错误
  8. 以上皆非

Q7-6.

以下哪个方法将无法编译?

  1. private void method1(String name) {
        if (name.equals("star"))
            throw new IllegalArgumentException(name);
    }
    
  2. private void method2(int age) {
        if (age > 30)
            throw Exception();
    }
    
  3. public Object method3(boolean accept) {
        if (accept)
            throw new StackOverflowError();
        else
            return new StackOverflowError();
    }
    
  4. protected double method4() throws Exception {
        throw new Throwable();
    }
    
  5. public double method5() throws Exception {
        return 0.7;
    }
    

Q7-7.

以下代码的输出是什么?

 class TryFinally {
    int tryAgain() {
        int a = 10;
        try {
            ++a;
        } finally {
            a++;
        }
        return a;
    }
    public static void main(String args[]) {
        System.out.println(new TryFinally().tryAgain());
    }
}
  1. 10
  2. 11
  3. 12
  4. 编译错误
  5. 运行时异常

Q7-8.

以下代码的输出是什么?

class EJavaBase {
    void myMethod() throws ExceptionInInitializerError {
        System.out.println("Base");
    }
}
class EJavaDerived extends EJavaBase {
    void myMethod() throws RuntimeException {
        System.out.println("Derived");
    }
}
class EJava3 {
    public static void main(String args[]) {
        EJavaBase obj = new EJavaDerived();
        obj.myMethod();
    }
}
  1. Base
    
  2. Derived
    
  3. Derived
    Base
    
  4. Base
    Derived
    
  5. 编译错误

Q7-9.

以下哪个陈述是正确的?

  1. 用户定义的类可能不会抛出 IllegalStateException。它必须仅由 Java API 类抛出。
  2. 如果将未初始化的 String 类型的实例变量传递给 System.out.println 来打印其值,它将抛出 NullPointerException
  3. 当将无效数字作为 String 传递给 Java API 的多个方法以转换为指定的数字格式时,会抛出 NumberFormatException
  4. 当你的代码中的 static 初始化器抛出 NullPointerException 时,JVM 可能会抛出 ExceptionInInitializerError

Q7-10.

以下代码的输出是什么?

class EJava {
    void foo() {
        try {
            String s = null;
            System.out.println("1");
            try {
                System.out.println(s.length());
            } catch (NullPointerException e) {
                System.out.println("inner");
            }
            System.out.println("2");
        } catch (NullPointerException e) {
            System.out.println("outer");
        }
    }
    public static void main(String args[]) {
        EJava obj = new EJava();
        obj.foo();
    }
}
  1. 1
    inner
    2
    outer
    
  2. 1
    outer
    
  3. 1
    inner
    
  4. 1
    inner
    2
    

7.9. 样本考试问题答案

Q7-1.

以下代码的输出是什么:

class Course {
    String courseName;
    Course() {
        Course c = new Course();
        c.courseName = "Oracle";
    }
}

class EJavaGuruPrivate {
    public static void main(String args[]) {
        Course c = new Course();
        c.courseName = "Java";
        System.out.println(c.courseName);
    }
}
  1. 代码将打印 Java
  2. 代码将打印 Oracle
  3. 代码将无法编译。
  4. 代码将在运行时抛出异常或错误。

答案:d

解释:此类将在运行时抛出 StackOverflowError。寻找 StackOverflowError 的最简单方法是在代码中定位递归方法调用。在问题的代码中,Course 类的构造函数创建了一个 Course 类的对象,这将再次调用构造函数。因此,这变成了一个递归调用,最终在运行时抛出 StackOverflowError。(正如你所知,异常或错误只能在运行时抛出,不能在编译时抛出。)

Q7-2.

选择正确的选项:

  1. 你无法处理运行时异常。
  2. 你不应该处理错误。
  3. 如果一个方法抛出检查型异常,它必须由该方法处理或指定在其 throws 子句中。
  4. 如果一个方法抛出运行时异常,它可以在其 throws 子句中包含该异常。
  5. 运行时异常是检查型异常。

答案:b, c, d

解释:选项 (a) 是不正确的。你可以像处理检查型异常一样处理运行时异常:使用 try-catch 块。

选项 (b) 是正确的。你不应该在代码中尝试处理错误。或者换句话说,当你的代码抛出错误时,你无法做太多。与其在代码中尝试处理错误,不如解决导致这些错误的代码。例如,StackOverflowError 是一个错误,如果你的代码在没有任何退出条件的情况下递归执行方法,它将被抛出。这种重复将消耗栈上的所有空间,并导致 StackOverflowError

选项 (c) 是正确的。如果你未能实现这些选项中的任何一个,你的代码将无法编译。

选项 (d) 是正确的。运行时异常不需要包含在方法的 throws 子句中。通常这种包含是不必要的,但如果你包含了它,你的代码将无任何问题地执行。

选项 (e) 是不正确的。运行时异常及其所有子类都不是检查型异常。

Q7-3.

检查以下代码并选择正确的选项:

class EJavaGuruExcep {
    public static void main(String args[]) {
        EJavaGuruExcep var = new EJavaGuruExcep();
        var.printArrValues(args);
    }
    void printArrValues(String[] arr) {
        try {
            System.out.println(arr[0] + ":" + arr[1]);
        } catch (NullPointerException e) {
            System.out.println("NullPointerException");
        } catch (IndexOutOfBoundsException e) {
            System.out.println("IndexOutOfBoundsException");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("ArrayIndexOutOfBoundsException");
        }
    }
}
  1. 如果使用以下命令执行类 EJavaGuruExcep,它将打印 NullPointerException:

  2. java EJavaGuruExcep
    
  3. 如果使用以下命令执行类 EJavaGuruExcep,它将打印 IndexOutOfBoundsException:

  4. java EJavaGuruExcep
    
  5. 如果使用以下命令执行类 EJavaGuruExcep,它将打印 ArrayIndexOutOfBoundsException:

  6. java EJavaGuruExcep one
    
  7. 代码将无法编译。

答案:d

解释:回答此问题的关键是要意识到以下两个事实:

  • 异常是类。如果使用异常的基类在 catch 块中,它可以捕获其所有派生类的异常。如果你之后尝试捕获其派生类的异常,代码将无法编译。
  • ArrayIndexOutOfBoundsExceptionIndexOutOfBoundsException 的一个派生类。

其余的要点试图让你相信问题基于传递给 main 方法的参数。

Q7-4.

以下代码的输出是什么?

class EJava {
    void method() {
        try {
            guru();
            return;
        } finally {
            System.out.println("finally 1");
        }
    }
    void guru() {
        System.out.println("guru");
        throw new StackOverflowError();
    }
    public static void main(String args[]) {
        EJava var = new EJava();
        var.method();
    }
}
  1. guru
    finally 1
    
  2. guru
    finally 1
    Exception in thread "main" java.lang.StackOverflowError
    
  3. guru
    Exception in thread "main" java.lang.StackOverflowError
    
  4. guru
    
  5. 代码无法编译。

答案:b

说明:代码中没有编译错误。

方法 guru 抛出 StackOverflowError,这不是一个检查型异常。即使你的代码不应该抛出错误,从语法上讲是可能的。你的代码将成功编译。

方法 guru 的调用紧随 return 关键字之后,本应结束 method 方法的执行。但是 guru 的调用位于一个 try-catch 块中,并包含一个 finally 块。由于 guru 没有自己处理 StackOverflowError 错误,控制流会寻找 method 方法中的异常处理器。调用该方法的方法没有处理这个错误,但定义了一个 finally 块。然后控制流执行 finally 块。由于代码找不到适当的处理器来处理这个错误,错误会传播到 JVM,导致代码突然中断。

Q7-5.

以下代码的输出是什么?

class Quest5 {
    public static void main(String args[]) {
        int arr[] = new int[5];
        arr = new int[]{1,2,3,4};

        int x = arr[1]-- + arr[0]-- /arr[0] * arr[4];
        System.out.println(x);
    }
}
  1. 代码输出一个值。
  2. 代码输出一个值后跟一个异常。
  3. ArithmeticException
  4. NullPointerException
  5. IndexOutOfBoundsException
  6. ArrayIndexOutOfBoundsException
  7. 编译错误
  8. 以上皆非

答案:c

说明:除了测试你的异常处理技能外,这个问题还测试了你在运算符优先级方面的能力。代码在尝试评估以下表达式时抛出 ArithmeticException

int x = arr[1]-- + arr[0]-- /arr[0] * arr[4];

在执行前面的代码行之前,arr[1] 存储的值是 2arr[0] 存储的值是 1,而 arr[4] 没有被初始化。因此尝试访问 arr[4] 将导致 ArrayIndexOutOfBoundsException

在算术运算中,后缀和前缀增量运算符具有最高的优先级。因此,第一次遍历将此方程简化为

int x = 2 + 1 /0 * undefined;

在这里,*/ 具有相同的优先级。除了运算符优先级之外,重要的是从左到右读取相同优先级的操作。这就是为什么在当前表达式中先计算 / 而不是 *。因此尝试执行 1/0 会抛出 ArithmeticException

Q7-6.

以下哪个方法无法编译?

  1. private void method1(String name) {
        if (name.equals("star"))
            throw new IllegalArgumentException(name);
    }
    
  2. private void method2(int age) {
        if (age > 30)
            throw Exception();
    }
    
  3. public Object method3(boolean accept) {
        if (accept)
            throw new StackOverflowError();
        else
            return new StackOverflowError();
    }
    
  4. protected double method4() throws Exception {
        throw new Throwable();
    }
    
  5. public double method5() throws Exception {
        return 0.7;
    }
    

答案:b, d

说明:编译成功的代码可能实现不正确。这个问题只询问那些遵循语法规则从而可以编译成功的代码。

选项 (a) 的代码可以成功编译。因为 IllegalArgumentException 是一个运行时异常,method1() 可以抛出它,而无需在它的 throws 语句中声明。

选项 (b) 的代码无法编译。method2() 抛出一个检查型异常,即 Exception,但没有在它的 throws 语句中声明。

尽管选项 (c) 中的代码意义不大,但它可以成功编译。一个方法可以抛出 StackOverflowError(一个未检查的异常),而无需将其包含在其方法声明的 throws 子句中。

选项 (d) 的代码不会编译。如果一个方法声明抛出已检查的异常,其方法体不能抛出更一般的异常。method4() 声明抛出 Exception 但抛出 Throwable,这是不允许的(ExceptionThrowable 的子类)。

选项 (e) 的代码可以成功编译。如果一个方法声明抛出 Exception,它实际上可能不会抛出它。这仅适用于 Exception(因为 RuntimeException 是它的子类)、运行时异常和错误。

Q7-7.

以下代码的输出是什么?

class TryFinally {
    int tryAgain() {
        int a = 10;
        try {
            ++a;
        } finally {
            a++;
        }
        return a;
    }

    public static void main(String args[]) {
        System.out.println(new TryFinally().tryAgain());
    }
}
  1. 10
  2. 11
  3. 12
  4. 编译错误
  5. 运行时异常

答案:c

解释:try 块执行,变量 a 的值增加到 11。此步骤之后是执行 finally 块,它也将变量 a 的值增加 1,变为 12tryAgain 方法返回值 12,由 main 方法打印。

代码没有编译问题。一个 try 块可以跟一个 finally 块,而不需要任何 catch 块。即使 try 块没有抛出任何异常,它也可以成功编译。以下是一个不会编译的 try-catch 块的例子,因为它试图 捕获 一个 try 块永远不会抛出的已检查的异常:

try {
    ++a;
} catch (java.io.FileNotFoundException e) {
}

Q7-8.

以下代码的输出是什么?

class EJavaBase {
    void myMethod() throws ExceptionInInitializerError {
        System.out.println("Base");
    }
}
class EJavaDerived extends EJavaBase {
    void myMethod() throws RuntimeException {
        System.out.println("Derived");
    }
}
class EJava3 {
    public static void main(String args[]) {
        EJavaBase obj = new EJavaDerived();
        obj.myMethod();
    }
}
  1. Base
  2. Derived
  3. Derived Base
  4. Base
    Derived
    
  5. 编译错误

答案:b

解释:如果基类方法不抛出异常,则派生类中重写的方法也不能抛出异常的规则仅适用于已检查的异常。它不适用于运行时(未检查)异常或错误。基类或重写的方法可以自由地抛出任何 error 或运行时异常。

Q7-9.

以下哪个陈述是正确的?

  1. 用户定义的类不能抛出 IllegalStateException。它只能由 Java API 类抛出。
  2. 如果将未初始化的 String 类型的实例变量传递给 System.out.println 以打印其值,它将抛出 NullPointerException
  3. NumberFormatException 在 Java API 的多个方法中抛出,当将无效数字作为 String 传递以转换为指定的数字格式时。
  4. 当你的代码中的 static 初始化器抛出 NullPointerException 时,JVM 可能会抛出 ExceptionInInitializerError

答案:c, d

选项 (a) 是不正确的。用户定义的类可以抛出 Java API 中的任何异常。

选项 (b) 是错误的。未初始化的类型为 String 的实例变量将被分配一个默认值 null。当你将这个变量传递给 System.out.println 来打印它时,它会打印 null。如果你尝试访问这个 null 对象的任何非静态成员(变量或方法),则会抛出 NullPointerException

Q7-10.

以下代码的输出是什么?

class EJava {
    void foo() {
        try {
            String s = null;
            System.out.println("1");
            try {
                System.out.println(s.length());
            } catch (NullPointerException e) {
                System.out.println("inner");
            }
            System.out.println("2");

        } catch (NullPointerException e) {
            System.out.println("outer");
        }
    }
    public static void main(String args[]) {
        EJava obj = new EJava();
        obj.foo();
    }
}
  1. 1
    inner
    2
    outer
    
  2. 1
    outer
    
  3. 1
    inner
    
  4. 1
    inner
    2
    

答案:d

解释:首先,嵌套的 try-catch 语句不会抛出编译错误。

因为变量 s 没有被初始化,尝试访问其方法 length() 将会抛出 NullPointerException。内部的 try-catch 块处理了这个异常并打印了 inner。然后控制权转移到外部 try-catch 块的剩余代码,打印了 2。因为 NullPointerException 已经在内部 try-catch 块中被处理,所以它不会被外部 try-catch 块处理。

第八章. 完整模拟考试

本章涵盖

  • 完整模拟考试,共 77 题

  • 所有模拟考试问题的答案,包括详细的解释和每个考试问题所基于的子目标

在实际考试中,每个问题都会显示你应该选择的正确选项的数量。考试引擎不会允许你选择比这个数字更多的答案选项。如果你尝试这样做,将会显示警告。这个模拟考试中的问题也指定了正确的答案选项数量,以便更接近实际考试。

8.1. 模拟考试

ME-Q1)

给定以下AnimalLionJumpable类的定义,选择不会导致编译错误或运行时异常的变量赋值组合(选择 2 个选项)。

interface Jumpable {}
class Animal {}
class Lion extends Animal implements Jumpable {}
  1. Jumpable var1 = new Jumpable();
  2. Animal var2 = new Animal();
  3. Lion var3 = new Animal();
  4. Jumpable var4 = new Animal();
  5. Jumpable var5 = new Lion();
  6. Jumpable var6 = (Jumpable)(new Animal());

ME-Q2)

给定以下代码,如果用以下选项替换/* INSERT CODE HERE */,将使代码打印1?(选择 1 个选项。)

try {
    String[][] names = {{"Andre", "Mike"}, null, {"Pedro"}};
    System.out.println (names[2][1].substring(0, 2));
} catch (/*INSERT CODE HERE*/) {
    System.out.println(1);
}
  1. IndexPositionException e
  2. NullPointerException e
  3. ArrayIndexOutOfBoundsException e
  4. ArrayOutOfBoundsException e

ME-Q3)

以下代码的输出是什么?(选择 1 个选项。)

public static void main(String[] args) {
    int a = 10; String name = null;
    try {
        a = name.length();                   //line1
        a++;                                 //line2
    } catch (NullPointerException e){
        ++a;
        return;
    } catch (RuntimeException e){
        a--;
        return;
    } finally {
        System.out.println(a);
    }
}
  1. 5
  2. 6
  3. 10
  4. 11
  5. 12
  6. 编译错误
  7. 没有输出
  8. 运行时异常

ME-Q4)

给定以下类定义,

class Student { int marks = 10; }

以下代码的输出是什么?(选择 1 个选项。)

class Result {
    public static void main(String... args) {
        Student s = new Student();
        switch (s.marks) {
            default: System.out.println("100");
            case 10: System.out.println("10");
            case 98: System.out.println("98");
        }
    }
}
  1. 100
    10
    98
    
  2. 10
    98
    
  3. 100
    
  4. 10
    

ME-Q5)

给定以下代码,以下哪个代码可以用来创建并初始化ColorPencil类的对象?(选择 2 个选项。)

class Pencil {}
class ColorPencil extends Pencil {
    String color;
    ColorPencil(String color) {this.color = color;}
}
  1. ColorPencil var1 = new ColorPencil();
  2. ColorPencil var2 = new ColorPencil(RED);
  3. ColorPencil var3 = new ColorPencil("RED");
  4. Pencil var4 = new ColorPencil("BLUE");

ME-Q6)

以下代码的输出是什么?(选择 1 个选项。)

class Doctor {
    protected int age;
    protected void setAge(int val) { age = val; }
    protected int getAge() { return age; }
}
class Surgeon extends Doctor {
    Surgeon(String val) {
        specialization = val;
    }

    String specialization;
    String getSpecialization() { return specialization; }
}
class Hospital {
    public static void main(String args[]) {
        Surgeon s1 = new Surgeon("Liver");
        Surgeon s2 = new Surgeon("Heart");
        s1.age = 45;
        System.out.println(s1.age + s2.getSpecialization());
        System.out.println(s2.age + s1.getSpecialization());
    }
}
  1. 45Heart
    0Liver
    
  2. 45Liver
    0Heart
    
  3. 45Liver
    45Heart
    
  4. 45Heart
    45Heart
    
  5. 类无法编译。

ME-Q7)

以下代码的输出是什么?(选择 1 个选项。)

class RocketScience {
    public static void main(String args[]) {
        int a = 0;
        while (a == a++) {
            a++;
            System.out.println(a);
        }
    }
}
  1. while循环不会执行;不会打印任何内容。
  2. while循环将无限执行,从1开始打印所有数字。
  3. while循环将无限执行,从0开始打印所有偶数。
  4. while循环将无限执行,从2开始打印所有偶数。
  5. while循环将无限执行,从1开始打印所有奇数。
  6. while循环将无限执行,从3开始打印所有奇数。

ME-Q8)

给定以下语句,

  • com.ejava是一个包
  • Person类定义在com.ejava包中
  • Course类定义在com.ejava包中

以下哪个选项正确地导入了 PersonCourse 类到 MyEJava 类中?(选择 3 个选项。)

  1. import com.ejava.*;
    class MyEJava {}
    
  2. import com.ejava;
    class MyEJava {}
    
  3. import com.ejava.Person;
    import com.ejava.Course;
    class MyEJava {}
    
  4. import com.ejava.Person;
    import com.ejava.*;
    class MyEJava {}
    

ME-Q9)

假设以下 AnimalForest 类定义在同一个包中,检查代码并选择正确的语句(选择 2 个选项)。

line1>    class Animal {
line2>        public void printKing() {
line3>            System.out.println("Lion");
line4>        }
line5>    }

line6>    class Forest {
line7>        public static void main(String... args) {
line8>            Animal anAnimal = new Animal();
line9>            anAnimal.printKing();
line10>        }
line11>    }
  1. Forest 打印 Lion

  2. 如果将第 2 行的代码更改为以下内容,类 Forest 将打印 Lion

  3. private void printKing() {
    
  4. 如果将第 2 行的代码更改为以下内容,类 Forest 将打印 Lion

  5. void printKing() {
    
  6. 如果将第 2 行的代码更改为以下内容,类 Forest 将打印 Lion

  7. default void printKing() {
    

ME-Q10)

给定以下代码,

class MainMethod {
    public static void main(String... args) {
        System.out.println(args[0]+":"+ args[2]);
    }
}

使用以下命令执行时,其输出是什么?(选择 1 个选项。)

java MainMethod 1+2 2*3 4-3 5+1
  1. java:1+2
  2. java:3
  3. MainMethod:2*3
  4. MainMethod:6
  5. 1+2:2*3
  6. 3:3
  7. 6
  8. 1+2:4-3
  9. 31
  10. 4

ME-Q11)

以下代码的输出是什么?(选择 1 个选项。)

interface Moveable {
    int move(int distance);
}
class Person {
    static int MIN_DISTANCE = 5;
    int age;
    float height;
    boolean result;
    String name;
}
public class EJava {
    public static void main(String arguments[]) {
        Person person = new Person();
        Moveable moveable = (x) -> Person.MIN_DISTANCE + x;
        System.out.println(person.name + person.height + person.result
                                       + person.age + moveable.move(20));
    }
}
  1. null0.0false025
  2. null0false025
  3. null0.0ffalse025
  4. 0.0false025
  5. 0false025
  6. 0.0ffalse025
  7. null0.0true025
  8. 0true025
  9. 0.0ftrue025
  10. 编译错误
  11. 运行时异常

ME-Q12)

给定以下代码,如果用以下选项替换 /* INSERT CODE HERE */,将使代码打印变量 pagesPerMin 的值?(选择 1 个选项。)

class Printer {
    int inkLevel;
}
class LaserPrinter extends Printer {
    int pagesPerMin;
    public static void main(String args[]) {
        Printer myPrinter = new LaserPrinter();
        System.out.println(/* INSERT CODE HERE */);
    }
}
  1. (LaserPrinter)myPrinter.pagesPerMin
  2. myPrinter.pagesPerMin
  3. LaserPrinter.myPrinter.pagesPerMin
  4. ((LaserPrinter)myPrinter).pagesPerMin

ME-Q13)

以下代码的输出是什么?(选择 1 个选项。)

interface Keys {
    String keypad(String region, int keys);
}
public class Handset {
    public static void main(String... args) {
        double price;
        String model;
        Keys varKeys = (region, keys) ->
                       {if (keys >= 32)
                        return region; else return "default";};
        System.out.println(model + price + varKeys.keypad("AB", 32));
    }
}
  1. null0AB
  2. null0.0AB
  3. null0default
  4. null0.0default
  5. 0
  6. 0.0
  7. 编译错误

ME-Q14)

以下代码的输出是什么?(选择 1 个选项。)

public class Sales {
    public static void main(String args[]) {
        int salesPhone = 1;
        System.out.println(salesPhone++ + ++salesPhone +
                                                       ++salesPhone);
    }
}
  1. 5
  2. 6
  3. 8
  4. 9

ME-Q15)

以下哪个选项定义了编译成功的 Java 类的正确结构?(选择 1 个选项。)

  1. package com.ejava.guru;
    package com.ejava.oracle;
    class MyClass {
        int age = /* 25 */ 74;
    }
    
  2. import com.ejava.guru.*;
    import com.ejava.oracle.*;
    package com.ejava;
    class MyClass {
        String name = "e" + "Ja /*va*/ v";
    }
    
  3. class MyClass {
        import com.ejava.guru.*;
    }
    
  4. class MyClass {
        int abc;
        String course = //this is a comment
                     "eJava";
    }
    
  5. 以上皆非

ME-Q16)

以下代码的输出是什么?(选择 1 个选项。)

class OpPre {
    public static void main(String... args) {
        int x = 10;
        int y = 20;

        int z = 30;
        if (x+y%z > (x+(-y)*(-z))) {
            System.out.println(x + y + z);
        }
    }
}
  1. 60
  2. 59
  3. 61
  4. 没有输出。
  5. 代码无法编译。

ME-Q17)

选择变量 name 的最合适的定义以及它应该声明的行号,以便以下代码编译成功(选择 1 个选项)。

class EJava {
    // LINE 1
    public EJava() {
        System.out.println(name);
    }
    void calc() {
        // LINE 2
        if (8 > 2) {
            System.out.println(name);
        }
    }
    public static void main(String... args) {
        // LINE 3
        System.out.println(name);
    }
}
  1. 在第 1 行定义 static String name;
  2. 在第 1 行定义 String name;
  3. 在第 2 行定义 String name;
  4. 在第 3 行定义 String name;

ME-Q18)

检查以下代码并选择正确的语句(选择 1 个选项)。

line1>    class Emp {
line2>        Emp mgr = new Emp();
line3>    }
line4>    class Office {
line5>        public static void main(String args[]) {
line6>            Emp e = null;
line7>            e = new Emp();
line8>            e = null;
line9>        }
line10>    }
  1. 对象 e 在第 8 行可被垃圾回收。
  2. 对象 e 在第 9 行可被垃圾回收。
  3. 对象 e 不可被垃圾回收,因为其成员变量 mgr 没有设置为 null
  4. 代码抛出运行时异常,代码执行永远不会到达第 8 行或第 9 行。

ME-Q19)

给定以下,

long result;

哪些选项是接受两个 String 参数和一个 int 参数的方法的正确声明,并且其返回值可以赋给变量 result?(选择 3 个选项。)

  1. Short myMethod1(String str1, int str2, String str3)
  2. Int myMethod2(String val1, int val2, String val3)
  3. Byte myMethod3(String str1, str2, int a)
  4. Float myMethod4(String val1, val2, int val3)
  5. Long myMethod5(int str2, String str3, String str1)
  6. Long myMethod6(String... val1, int val2)
  7. Short myMethod7(int val1, String... val2)

ME-Q20)

以下哪个选项可以成功编译?(选择 3 个选项。)

  1. int eArr1[] = {10, 23, 10, 2};
  2. int[] eArr2 = new int[10];
  3. int[] eArr3 = new int[] {};
  4. int[] eArr4 = new int[10] {};
  5. int eArr5[] = new int[2] {10, 20};

ME-Q21)

假设 Oracle 要求你创建一个返回两个 String 对象连接值的函数。以下哪个函数可以完成这项任务?(选择 2 个选项。)

  1. public String add(String 1, String 2) {
        return str1 + str2;
    }
    
  2. private String add(String s1, String s2) {
        return s1.concat(s2);
    }
    
  3. protected String add(String value1, String value2) {
        return value2.append(value2);
    }
    
  4. String subtract(String first, String second) {
        return first.concat(second.substring(0));
    }
    

ME-Q22)

给定以下,

int ctr = 10;
char[] arrC1 = new char[]{'P','a','u','l'};
char[] arrC2 = {'H','a','r','r','y'};
//INSERT CODE HERE
System.out.println(ctr);

哪些选项,当插入到 //INSERT CODE HERE 时,将输出 14?(选择 2 个选项。)

  1. for (char c1 : arrC1) {
        for (char c2 : arrC2) {
            if (c2 == 'a') break;
            ++ctr;
        }
    }
    
  2. for (char c1 : arrC1)
        for (char c2 : arrC2) {
            if (c2 == 'a') break;
            ++ctr;
        }
    
  3. for (char c1 : arrC1)
        for (char c2 : arrC2)
            if (c2 == 'a') break;
            ++ctr;
    
  4. for (char c1 : arrC1) {
        for (char c2 : arrC2) {
            if (c2 == 'a') continue;
            ++ctr;
        }
    }
    

ME-Q23)

给定以下对类 ChemistryBook 的定义,选择正确的语句(选择 2 个选项)。

import java.util.ArrayList;
class ChemistryBook {
    public void read() {}                //METHOD1
    public String read() { return null; }     //METHOD2
    ArrayList read(int a) { return null; }     //METHOD3
}
  1. 标记为 //METHOD1//METHOD2 的方法是正确重载的方法。
  2. 标记为 //METHOD2//METHOD3 的方法是正确重载的方法。
  3. 标记为 //METHOD1//METHOD3 的方法是正确重载的方法。
  4. 所有方法——标记为 //METHOD1//METHOD2//METHOD3 的方法——都是正确重载的方法。

ME-Q24)

给定以下,

final class Home {
    String name;
    int rooms;
    //INSERT CONSTRUCTOR HERE
}

哪些选项,当插入到 //INSERT CONSTRUCTOR HERE 时,将为类 Home 定义有效的重载构造函数?(选择 3 个选项。)

  1. Home() {}
  2. Float Home() {}
  3. protected Home(int rooms) {}
  4. final Home() {}
  5. private Home(long name) {}
  6. float Home(int rooms, String name) {}
  7. static Home() {}

ME-Q25)

给定以下代码,以下哪个选项,如果用于替换 // INSERT CODE HERE,将使代码打印出完全能被 14 整除的数字?(选择 1 个选项。)

for (int ctr = 2; ctr <= 30; ++ctr) {
    if (ctr % 7 != 0)
        //INSERT CODE HERE
    if (ctr % 14 == 0)
        System.out.println(ctr);
}
  1. continue;
  2. exit;
  3. break;
  4. end;

ME-Q26)

以下代码的输出是什么?(选择 1 个选项。)

import java.util.function.Predicate;
public class MyCalendar {
    public static void main(String arguments[]) {
        Season season1 = new Season();
        season1.name = "Spring";

        Season season2 = new Season();
        season2.name = "Autumn";

        Predicate<String> aSeason = (s) -> s == "Summer" ?
                                season1.name : season2.name;
        season1 = season2;
        System.out.println(season1.name);
        System.out.println(season2.name);
        System.out.println(aSeason.test(new String("Summer")));
    }
}
class Season {
    String name;
}
  1. String
    Autumn
    false
    
  2. Spring
    String
    false
    
  3. Autumn
    Autumn
    false
    
  4. Autumn
    String
    true
    
  5. 编译错误
  6. 运行时异常

ME-Q27)

以下代码的哪个说法是正确的?(选择 1 个选项。)

class Shoe {}
class Boot extends Shoe {}
class ShoeFactory {
    ShoeFactory(Boot val) {
        System.out.println("boot");
    }
    ShoeFactory(Shoe val) {
        System.out.println("shoe");
    }
}
  1. ShoeFactory 总共有两个重载构造函数。
  2. ShoeFactory 有三个重载构造函数,两个用户定义构造函数和一个默认构造函数。
  3. ShoeFactory 类将无法编译。
  4. 添加以下构造函数将使 ShoeFactory 类的构造函数数量增加到 3 个:
  5. private ShoeFactory (Shoe arg) {}
    

ME-Q28)

给定 ColorPencilTestColor 类的定义,以下哪个选项,如果用于替换 //INSERT CODE HERE,将初始化引用变量 myPencil 的实例变量 color 为字符串字面值 "RED"?(选择 1 个选项。)

class ColorPencil {
    String color;
    ColorPencil(String color) {
        //INSERT CODE HERE
    }
}
class TestColor {
    ColorPencil myPencil = new ColorPencil("RED");
}
  1. this.color = color;
  2. color = color;
  3. color = RED;
  4. this.color = RED;

ME-Q29)

以下代码的输出是什么?(选择 1 个选项。)

class EJavaCourse {
    String courseName = "Java";
}
class University {
    public static void main(String args[]) {
        EJavaCourse courses[] = { new EJavaCourse(), new EJavaCourse() };
        courses[0].courseName = "OCA";
        for (EJavaCourse c : courses) c = new EJavaCourse();
        for (EJavaCourse c : courses) System.out.println(c.courseName);
    }
}
  1. Java
    Java
    
  2. OCA
    Java
    
  3. OCA
    OCA
    
  4. 以上皆非

ME-Q30)

以下代码的输出是什么?(选择 1 个选项。)

class Phone {
    static void call() {
        System.out.println("Call-Phone");
    }

}
class SmartPhone extends Phone{
    static void call() {
        System.out.println("Call-SmartPhone");
    }
}
class TestPhones {
    public static void main(String... args) {
        Phone phone = new Phone();
        Phone smartPhone = new SmartPhone();
        phone.call();
        smartPhone.call();
    }
}
  1. Call-Phone
    Call-Phone
    
  2. Call-Phone
    Call-SmartPhone
    
  3. Call-Phone
    null
    
  4. null
    Call-SmartPhone
    

ME-Q31)

给定以下代码,以下哪些语句是正确的?(选择 3 个选项。)

class MyExam {
    void question() {
        try {
            question();
        } catch (StackOverflowError e) {
            System.out.println("caught");
        }
    }
    public static void main(String args[]) {
        new MyExam().question();
    }
}
  1. 代码将打印 caught
  2. 代码不会打印 caught
  3. 如果 StackOverflowError 是运行时异常,代码将打印 caught
  4. 如果 StackOverflowError 是一个检查型异常,代码将打印 caught
  5. 如果 question() 抛出 NullPointer-Exception 异常,代码将打印 caught

ME-Q32)

Student 类被定义为以下形式:

public class Student {
    private String fName;
    private String lName;

    public Student(String first, String last) {
        fName = first; lName = last;
    }
    public String getName() { return fName + lName; }
}

类的创建者后来将方法 getName 改变为以下形式:

public String getName() {
    return fName + " " + lName;
}

这个变化的含义是什么?(选择 2 个选项。)

  1. 使用 Student 类的类将无法编译。
  2. 使用 Student 类的类将无需任何编译问题而工作。
  3. Student 类是一个封装良好的类的例子。
  4. Student 类将其实例变量暴露在类外部。

ME-Q33)

以下代码的输出是什么?(选择 1 个选项。)

class ColorPack {
    int shadeCount = 12;
    static int getShadeCount() {
        return shadeCount;
    }
}
class Artist {
    public static void main(String args[]) {
        ColorPack pack1 = new ColorPack();
        System.out.println(pack1.getShadeCount());
    }
}
  1. 10
  2. 12
  3. 没有输出
  4. 编译错误

ME-Q34)

保罗定义了他的 LaptopWorkshop 类来升级他的笔记本电脑的内存。你认为他成功了么?以下代码的输出是什么?(选择 1 个选项。)

class Laptop {
    String memory = "1 GB";
}
class Workshop {
    public static void main(String args[]) {
        Laptop life = new Laptop();
        repair(life);

        System.out.println(life.memory);
    }
    public static void repair(Laptop laptop) {
        laptop.memory = "2 GB";
    }
}
  1. 1 GB
  2. 2 GB
  3. 编译错误
  4. 运行时异常

ME-Q35)

以下代码的输出是什么?(选择 1 个选项。)

public class Application {
    public static void main(String... args) {
        double price = 10;
        String model;
        if (price > 10)
            model = "Smartphone";
        else if (price <= 10)
            model = "landline";
        System.out.println(model);
    }
}
  1. landline
  2. Smartphone
  3. 没有输出
  4. 编译错误

ME-Q36)

以下代码的输出是什么?(选择 1 个选项。)

class EString {
    public static void main(String args[]) {
        String eVal = "123456789";
        System.out.println(eVal.substring(eVal.indexOf("2"),
        eVal.indexOf("0")).concat("0"));
    }
}
  1. 234567890
  2. 34567890
  3. 234456789
  4. 3456789
  5. 编译错误
  6. 运行时异常

ME-Q37)

检查以下代码并选择正确的语句(选择 2 个选项)。

class Artist {
    Artist assistant;
}
class Studio {
    public static void main(String... args) {
        Artist a1 = new Artist();
        Artist a2 = new Artist();
        a2.assistant = a1;
        a2 = null;        // Line 1
    }
     // Line 2
}
  1. 至少有两个对象在第 1 行被垃圾回收。
  2. 至少有一个对象在第 1 行被垃圾回收。
  3. 第 1 行没有对象被垃圾回收。
  4. 第 1 行被垃圾回收的对象数量是未知的。
  5. 至少有两个对象在第 2 行有资格被垃圾回收。

ME-Q38)

以下代码的输出是什么?(选择 1 个选项。)

class Book {
    String ISBN;
    Book(String val) {
        ISBN = val;
    }
}
class TestEquals {
    public static void main(String... args) {
        Book b1 = new Book("1234-4657");
        Book b2 = new Book("1234-4657");
        System.out.print(b1.equals(b2) +":");
        System.out.print(b1 == b2);
    }
}
  1. true:false
  2. true:true
  3. false:true
  4. false:false
  5. 编译错误——在Book类中没有equals方法。
  6. 运行时异常。

ME-Q39)

以下哪个陈述是正确的?(选择 2 个选项。)

  1. StringBuilder sb1 = new StringBuilder()将创建一个没有字符但具有存储 16 个字符的初始容量的StringBuilder对象。
  2. StringBuilder sb1 = new StringBuilder(5*10)将创建一个具有值50StringBuilder对象。
  3. String类不同,StringBuilder中的concat方法会修改StringBuilder对象的值。
  4. insert方法可以用来在StringBuilder的开始、结束或指定位置插入字符、数字或String

ME-Q40)

给定以下Animal类和Jump接口的定义,选择正确的数组声明和初始化(选择 3 个选项)。

interface Jump {}
class Animal implements Jump {}
  1. Jump eJump1[] = {null, new Animal()};
  2. Jump[] eJump2 = new Animal()[22];
  3. Jump[] eJump3 = new Jump[10];
  4. Jump[] eJump4 = new Animal[87];
  5. Jump[] eJump5 = new Jump()[12];

ME-Q41)

以下代码的输出是什么?(选择 1 个选项。)

import java.util.*;
class EJGArrayL {
    public static void main(String args[]) {
        ArrayList<String> seasons = new ArrayList<>();
        seasons.add(1, "Spring"); seasons.add(2, "Summer");
        seasons.add(3, "Autumn"); seasons.add(4, "Winter");
        seasons.remove(2);

        for (String s : seasons)
            System.out.print(s + ", ");
    }
}
  1. Spring, Summer, Winter,
  2. Spring, Autumn, Winter,
  3. Autumn, Winter,
  4. 编译错误
  5. 运行时异常

ME-Q42)

以下代码的输出是什么?(选择 1 个选项。)

class EIf {
    public static void main(String args[]) {
        bool boolean = false;
        do {
            if (boolean = true)
                System.out.println("true");
            else
                System.out.println("false");
        }
        while(3.3 + 4.7 > 8);    }
}
  1. 该类将打印true
  2. 该类将打印false
  3. 如果将if条件改为boolean == true,则该类将打印true
  4. 如果将if条件改为boolean != true,则该类将打印false
  5. 该类无法编译。
  6. 运行时异常。

ME-Q43)

Whale(如下定义)成功吃掉了多少条Fish?检查以下代码并选择正确的陈述(选择 2 个选项)。

class Whale {
    public static void main(String args[]) {
        boolean hungry = false;
        while (hungry=true) {
            ++Fish.count;
        }
        System.out.println(Fish.count);
    }
}
class Fish {
    static byte count;
}
  1. 代码无法编译。
  2. 代码不打印任何值。
  3. 代码打印0
  4. ++Fish.count改为Fish.count++将给出相同的结果。

ME-Q44)

给定以下代码,如果用以下哪个选项替换/* REPLACE CODE HERE */,将使代码打印出存储在数组phones中的位置上的电话名称?(选择 1 个选项。)

class Phones {
    public static void main(String args[]) {
        String phones[]= {"BlackBerry", "Android", "iPhone"};

        for (String phone : phones)
            /* REPLACE CODE HERE */
    }
}
  1. System.out.println(phones.count + ":" + phone);
  2. System.out.println(phones.counter + ":" + phone);
  3. System.out.println(phones.getPosition() + ":" + phone);
  4. System.out.println(phones.getCtr() + ":" + phone);
  5. System.out.println(phones.getCount() + ":" + phone);
  6. System.out.println(phones.pos + ":" + phone);
  7. 以上皆非

ME-Q45)

给定以下代码,

Byte b1 = (byte)100;                       // 1
Integer i1 = (int)200;                     // 2
Long l1 = (long)300;                       // 3
Float f1 = (float)b1 + ( 
     0int)l1;            // 4
String s1 = 300;                           // 5
if (s1 == (b1 + i1))                       // 6
    s1 = (String)500;                      // 7
else                                       // 8
    f1 = (int)100;                         // 9
System.out.println(s1 + ":" + f1);         // 10

输出是什么?选择 1 个选项。

  1. 代码在第 1、3、4、7 行处失败编译。
  2. 代码在第 6 行、第 7 行处编译失败。
  3. 代码在第 7 行、第 9 行处编译失败。
  4. 代码在第 4 行、第 5 行、第 6 行、第 7 行、第 9 行处编译失败。
  5. 没有编译错误——输出 500:300
  6. 没有编译错误——输出 300:100
  7. 运行时异常。

ME-Q46)

以下代码的输出是什么?(选择 1 个选项。)

class Book {
    String ISBN;
    Book(String val) {
        ISBN = val;
    }
    public boolean equals(Object b) {
        if (b instanceof Book) {
            return ((Book)b).ISBN.equals(ISBN);
        }

        else
            return false;
    }
}

class TestEquals {
    public static void main(String args[]) {
        Book b1 = new Book("1234-4657");
        Book b2 = new Book("1234-4657");
        LocalDate release = null;
        release = b1.equals(b2) ? b1 == b2? LocalDate.of(2050,12,12):
        LocalDate.parse("2072-02-01"):LocalDate.parse("9999-09-09");
        System.out.print(release);
    }
}
  1. 2050-12-12
  2. 2072-02-01
  3. 9999-09-09
  4. 编译错误
  5. 运行时异常

ME-Q47)

以下代码的输出是什么?(选择 1 个选项。)

int a = 10;
for (; a <= 20; ++a) {
    if (a%3 == 0) a++; else if (a%2 == 0) a=a*2;
    System.out.println(a);
}
  1. 11
    13
    15
    17
    19
    
  2. 20
    
  3. 11
    14
    17
    20
    
  4. 40
    
  5. 编译错误

ME-Q48)

给定以下代码,以下哪个选项,如果用于替换 // INSERT CODE HERE,将定义一个重载的 rideWave 方法?(选择 1 个选项。)

class Raft {
    public String rideWave() { return null; }
    //INSERT CODE HERE
}
  1. public String[] rideWave() { return null; }
  2. protected void riceWave(int a) {}
  3. private void rideWave(int value, String value2) {}
  4. default StringBuilder rideWave (StringBuffer a) { return null; }

ME-Q49)

给定以下代码,以下哪个选项,如果用于替换 // INSERT CODE HERE,将正确计算数组 num 中所有偶数的总和并将其存储在变量 sum 中?(选择 1 个选项。)

int num[] = {10, 15, 2, 17};
int sum = 0;
for (int number : num) {
    //INSERT CODE HERE
    sum += number;
}
  1. if (number % 2 == 0)
        continue;
    
  2. if (number % 2 == 0)
        break;
    
  3. if (number % 2 != 0)
        continue;
    
  4. if (number % 2 != 0)
        break;
    

ME-Q50)

以下代码的输出是什么?(选择 1 个选项。)

class Op {
    public static void main(String... args) {
        int a = 0;
        int b = 100;
        Predicate<Integer> compare = (var) -> var++ == 10;
        if (!b++ > 100 && compare.test(a)) {
            System.out.println(a+b);
        }
    }
}
  1. 100
  2. 101
  3. 102
  4. 代码编译失败。
  5. 没有输出。

ME-Q51)

选择符合以下规范的选项:创建一个封装良好的类 Pencil,其中有一个实例变量 modelmodel 的值应在 Pencil 外部可访问和修改。(选择 1 个选项。)

  1. class Pencil {
        public String model;
    }
    
  2. class Pencil {
        public String model;
        public String getModel() { return model; }
        public void setModel(String val) { model = val; }
    }
    
  3. class Pencil {
        private String model;
        public String getModel() { return model; }
        public void setModel(String val) { model = val; }
    }
    
  4. class Pencil {
        public String model;
        private String getModel() { return model; }
        private void setModel(String val) { model = val; }
    }
    

ME-Q52)

以下代码的输出是什么?(选择 1 个选项。)

class Phone {
    void call() {
        System.out.println("Call-Phone");
    }
}
class SmartPhone extends Phone{
    void call() {
        System.out.println("Call-SmartPhone");
    }
}
class TestPhones {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone smartPhone = new SmartPhone();
        phone.call();
        smartPhone.call();
    }
}
  1. Call-Phone
    Call-Phone
    
  2. Call-Phone
    Call-SmartPhone
    
  3. Call-Phone
    null
    
  4. null
    Call-SmartPhone
    

ME-Q53)

以下代码的输出是什么?(选择 1 个选项。)

class Phone {
    String keyboard = "in-built";
}
class Tablet extends Phone {
    boolean playMovie = false;
}
class College2 {
    public static void main(String args[]) {
        Phone phone = new Tablet();
        System.out.println(phone.keyboard + ":" + phone.playMovie);
    }
}
  1. in-built:false
  2. in-built:true
  3. null:false
  4. null:true
  5. 编译错误

ME-Q54)

以下代码的输出是什么?(选择 1 个选项。)

public class Wall {
    public static void main(String args[]) {
        double area = 10.98;
        String color;
        if (area < 5)
            color = "red";
        else
            color = "blue";
        System.out.println(color);
    }
}
  1. red
  2. blue
  3. 没有输出
  4. 编译错误

ME-Q55)

以下代码的输出是什么?(选择 1 个选项。)

class Diary {
    int pageCount = 100;
    int getPageCount() {
        return pageCount;
    }
    void setPageCount(int val) {
        pageCount = val;
    }
}

class ClassRoom {
    public static void main(String args[]) {
        System.out.println(new Diary().getPageCount());
        new Diary().setPageCount(200);
        System.out.println(new Diary().getPageCount());
    }
}
  1. 100
    200
    
  2. 100
    100
    
  3. 200
    200
    
  4. 代码编译失败。

ME-Q56)

你认为你可以用以下代码购物多少次(即以下代码的输出是什么)?(选择 1 个选项。)

class Shopping {
    public static void main(String args[]) {
        boolean bankrupt = true;
        do System.out.println("enjoying shopping"); bankrupt = false;
        while (!bankrupt);
    }
}
  1. 代码打印一次 enjoying shopping
  2. 代码打印两次 enjoying shopping
  3. 代码在无限循环中打印 enjoying shopping
  4. 代码编译失败。

ME-Q57)

以下哪个选项是定义多维数组的有效选项?(选择 4 个选项。)

  1. String ejg1[][] = new String[1][2];
  2. String ejg2[][] = new String[][] { {}, {} };
  3. String ejg3[][] = new String[2][2];
  4. String ejg4[][] = new String[][]{{null},new String[]{"a","b","c"}, {new String()}};
  5. String ejg5[][] = new String[][2];
  6. String ejg6[][] = new String[][]{"A", "B"};
  7. String ejg7[][] = new String[]{{"A"}, {"B"}};

ME-Q58

以下代码的输出是什么?(选择 1 个选项。)

class Laptop {
    String memory = "1GB";
}
class Workshop {
    public static void main(String args[]) {
        Laptop life = new Laptop();
        repair(life);
        System.out.println(life.memory);
    }
    public static void repair(Laptop laptop) {
        laptop = new Laptop();
        laptop.memory = "2GB";
    }
}
  1. 1 GB
  2. 2 GB
  3. 编译错误
  4. 运行时异常

ME-Q59

给定以下代码,以下哪个选项,如果用于替换//INSERT CODE HERE,将使类型为Roamable的引用变量能够引用Phone类的对象?(选择 1 个选项。)

interface Roamable{}
class Phone {}
class Tablet extends Phone implements Roamable {
    //INSERT CODE HERE
}
  1. Roamable var = new Phone();
  2. Roamable var = (Roamable)Phone();
  3. Roamable var = (Roamable)new Phone();
  4. 因为接口Roamable和类Phone没有关联,类型为Roamable的引用变量不能引用Phone类的对象。

ME-Q60

以下代码的输出是什么?(选择 1 个选项。)

class Paper {
    Paper() {
        this(10);
        System.out.println("Paper:0");
    }
    Paper(int a) { System.out.println("Paper:1"); }
}
class PostIt extends Paper {}

class TestPostIt {
    public static void main(String[] args) {
        Paper paper = new PostIt();
    }
}
  1. Paper:1
    
  2. Paper:0
    
  3. Paper:0
    Paper:1
    
  4. Paper:1
    Paper:0
    

ME-Q61

检查以下代码并选择正确的陈述(选择 1 个选项)。

line1> class StringBuilders {
line2>     public static void main(String... args) {
line3>         StringBuilder sb1 = new StringBuilder("eLion");
line4>         String ejg = null;
line5>         ejg = sb1.append("X").substring(sb1.indexOf("L"), sb1.indexOf("X"));
line6>         System.out.println(ejg);
line7>     }
line8> }
  1. 代码将打印LionX

  2. 代码将打印Lion

  3. 如果将第 5 行代码更改为以下内容,代码将打印Lion

  4. ejg = sb1.append("X").substring(sb1.indexOf('L'), sb1.indexOf('X'));
    
  5. 只有当第 4 行代码更改为以下内容时,代码才能编译:

  6. StringBuilder ejg = null;
    

ME-Q62

给定以下代码,

interface Jumpable {
    int height = 1;
    default void worldRecord() {
        System.out.print(height);
    }
}
interface Moveable {
    int height = 2;
    static void worldRecord() {
        System.out.print(height);
    }
}

class Chair implements Jumpable, Moveable {
    int height = 3;
    Chair() {
        worldRecord();
    }
    public static void main(String args[]) {
        Jumpable j = new Chair();
        Moveable m = new Chair();
        Chair c = new Chair();
    }
}

输出是什么?选择 1 个选项。

  1. 111
  2. 123
  3. 333
  4. 222
  5. 编译错误
  6. 运行时异常

ME-Q63

给定以下代码,以下哪个选项,如果用于替换/* INSERT CODE HERE */,将使类Jungle能够确定引用变量animal是否引用了Lion类的对象并打印1?(选择 1 个选项。)

class Animal{ float age; }
class Lion extends Animal { int claws;}
class Jungle {
    public static void main(String args[]) {
        Animal animal = new Lion();
        /* INSERT CODE HERE */
            System.out.println(1);
    }
}
  1. if (animal instanceof Lion)
  2. if (animal instanceOf Lion)
  3. if (animal == Lion)
  4. if (animal = Lion)

ME-Q64

假设文件Test.java定义了以下代码,但无法编译,请选择编译失败的原因(选择 2 个选项)。

class Person {
    Person(String value) {}
}
class Employee extends Person {}

class Test {
    public static void main(String args[]) {
        Employee e = new Employee();
    }
}
  1. Person无法编译。
  2. Employee无法编译。
  3. 默认构造函数只能调用基类的无参构造函数。
  4. 在类Test中创建Employee类对象的代码没有将String值传递给Employee类的构造函数。

ME-Q65

检查以下代码并选择正确的陈述(选择 2 个选项)。

class Bottle {
    void Bottle() {}
    void Bottle(WaterBottle w) {}
}
class WaterBottle extends Bottle {}
  1. 基类不能在其构造函数中将定义的类的引用变量作为方法参数传递。
  2. 类编译成功——基类可以使用其派生类的引用变量作为方法参数。
  3. Bottle定义了两个重载的构造函数。
  4. Bottle只能访问一个构造函数。

ME-Q66

给定以下代码,以下哪个选项,如果用于替换/* INSERT CODE HERE */,将导致代码打印110?(选择 1 个选项。)

class Book {
    private int pages = 100;
}
class Magazine extends Book {
    private int interviews = 2;
    private int totalPages() { /* INSERT CODE HERE */ }

    public static void main(String[] args) {
        System.out.println(new Magazine().totalPages());
    }

}
  1. return super.pages + this.interviews*5;
  2. return this.pages + this.interviews*5;
  3. return super.pages + interviews*5;
  4. return pages + this.interviews*5;
  5. 以上皆非

ME-Q67)

给定以下代码,

class NoInkException extends Exception {}
class Pen{
    void write(String val) throws NoInkException {
        int c = (10 - 7)/ (8 - 2 - 6);
    }
    void article() {
        //INSERT CODE HERE
    }
}

//INSERT CODE HERE 插入以下哪个选项将定义在 article 方法中有效使用 write 方法?(选择 2 个选项。)

  1. try {
        new Pen().write("story");
    } catch (NoInkException e) {}
    
  2. try {
        new Pen().write("story");
    } finally {}
    
  3. try {
        write("story");
    } catch (Exception e) {}
    
  4. try {
        new Pen().write("story");
    } catch (RuntimeException e) {}
    

ME-Q68)

以下代码的输出是什么?(选择 1 个选项。)

class EMyMethods {
    static String name = "m1";
    void riverRafting() {
        String name = "m2";
        if (8 > 2) {
            String name = "m3";
            System.out.println(name);
        }
    }
    public static void main(String[] args) {
        EMyMethods m1 = new EMyMethods();
        m1.riverRafting();
    }
}
  1. m1
  2. m2
  3. m3
  4. 代码无法编译。

ME-Q69)

以下代码的输出是什么?(选择 1 个选项。)

class EBowl {
    public static void main(String args[]) {
        String eFood = "Corn";
        System.out.println(eFood);
        mix(eFood);
        System.out.println(eFood);
    }
    static void mix(String foodIn) {
        foodIn.concat("A");
        foodIn.replace('C', 'B');
    }
}
  1. Corn
    BornA
    
  2. Corn
    CornA
    
  3. Corn
    Born
    
  4. Corn
    Corn
    

ME-Q70)

以下代码的哪个陈述是正确的?(选择 1 个选项。)

class SwJava {
    public static void main(String args[]) {
        String[] shapes = {"Circle", "Square", "Triangle"};
        switch (shapes) {
            case "Square": System.out.println("Circle"); break;
            case "Triangle": System.out.println("Square"); break;
            case "Circle": System.out.println("Triangle"); break;
        }
    }
}
  1. 代码打印 Circle

  2. 代码打印 Square

  3. 代码打印 Triangle

  4. 代码打印

  5. Circle
    Square
    Triangle
    
  6. 代码打印

  7. Triangle
    Circle
    Square
    
  8. 代码无法编译。

ME-Q71)

给定 PersonFatherHome 类的定义,以下哪个选项,如果用于替换 //INSERT CODE HERE,将导致代码成功编译?(选择 3 个选项。)

class Person {}
class Father extends Person {
    public void dance() throws ClassCastException {}
}
class Home {
    public static void main(String args[]) {
        Person p = new Person();
        try {
            ((Father)p).dance();
        }
        //INSERT CODE HERE
    }
}
  1. catch (NullPointerException e) {}
    catch (ClassCastException e) {}
    catch (Exception e) {}
    catch (Throwable t) {}
    
  2. catch (ClassCastException e) {}
    catch (NullPointerException e) {}
    catch (Exception e) {}
    catch (Throwable t) {}
    
  3. catch (ClassCastException e) {}
    catch (Exception e) {}
    catch (NullPointerException e) {}
    catch (Throwable t) {}
    
  4. catch (Throwable t) {}
    catch (Exception e) {}
    catch (ClassCastException e) {}
    catch (NullPointerException e) {}
    
  5. finally {}
    

ME-Q72)

以下代码的输出是什么?(选择 1 个选项。)

import java.time.*;
class Camera {
    public static void main(String args[]) {
        int hours;
        LocalDateTime now = LocalDateTime.of(2020, 10, 01, 0 , 0);
        LocalDate before = now.toLocalDate().minusDays(1);
        LocalTime after = now.toLocalTime().plusHours(1);

        while (before.isBefore(after) && hours < 4) {
            ++hours;
        }
        System.out.println("Hours:" + hours);
    }
}
  1. 代码打印 Camera:null
  2. 代码打印 Camera:Adjust settings manually
  3. 代码打印 Camera:
  4. 代码将无法编译。

ME-Q73)

TestEJavaCourse 的输出,如下定义,为 300

class Course {
    int enrollments;
}
class TestEJavaCourse {
    public static void main(String args[]) {
        Course c1 = new Course();
        Course c2 = new Course();
        c1.enrollments = 100;
        c2.enrollments = 200;
        System.out.println(c1.enrollments + c2.enrollments);
    }
}

如果将变量 enrollments 定义为 static 变量,会发生什么?(选择 1 个选项。)

  1. 输出无变化。TestEJavaCourse 打印 300
  2. 输出有变化。TestEJavaCourse 打印 200
  3. 输出有变化。TestEJavaCourse 打印 400
  4. TestEJavaCourse 无法编译。

ME-Q74)

以下代码的输出是什么?(选择 1 个选项。)

String ejgStr[] = new String[][]{{null},new String[]{"a","b","c"},{new String()}}[0] ;
String ejgStr1[] = null;
String ejgStr2[] = {null};

System.out.println(ejgStr[0]);
System.out.println(ejgStr2[0]);
System.out.println(ejgStr1[0]);
  1. null
    NullPointerException
    
  2. null
    null
    NullPointerException
    
  3. NullPointerException
    
  4. null
    null
    null
    

ME-Q75)

检查以下代码并选择正确的陈述(选择 1 个选项)。

import java.util.*;
class Person {}
class Emp extends Person {}

class TestArrayList {
    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<>();
        list.add(new String("1234"));                 //LINE1
        list.add(new Person());                       //LINE2
        list.add(new Emp());                          //LINE3
        list.add(new String[]{"abcd", "xyz"});        //LINE4
        list.add(LocalDate.now().plus(1));            //LINE5
    }
}
  1. 第 1 行的代码无法编译。
  2. 第 2 行的代码无法编译。
  3. 第 3 行的代码无法编译。
  4. 第 4 行的代码无法编译。
  5. 第 5 行的代码无法编译。
  6. 以上皆非。
  7. 从 (a) 到 (e) 的所有选项。

ME-Q76)

以下代码的输出是什么?(选择 1 个选项。)

public class If2 {
    public static void main(String args[]) {
        int a = 10; int b = 20; boolean c = false;
        if (b > a) if (++a == 10) if (c!=true) System.out.println(1);
        else System.out.println(2); else System.out.println(3);
    }
}
  1. 1
  2. 2
  3. 3
    
  4. 无输出

ME-Q77)

给定以下代码,

interface Movable {
    default int distance() {
        return 10;
    }
}
interface Jumpable {
    default int distance() {

        return 10;
    }
}

哪些选项正确地定义了实现接口 MovableJumpable 的类 Person?(选择 1 个选项。)

  1. class Person implements Movable, Jumpable {}
    
  2. class Person implements Movable, Jumpable {
        default int distance() {
            return 10;
        }
    }
    
  3. class Person implements Movable, Jumpable {
        public int distance() {
            return 10;
        }
    }
    
  4. class Person implements Movable, Jumpable {
        public long distance() {
            return 10;
        }
    }
    
  5. class Person implements Movable, Jumpable {
        int distance() {
            return 10;
        }
    }
    

8.2. 模拟考试题目的答案

本节包含 第 8.1 节 中所有模拟考试题目的答案。此外,每个问题都由基于的问题目标所引导。

[7.2] 编写代码以演示多态的使用;包括重写和对象类型与引用类型

[7.3] 确定何时需要类型转换

[8.5] “识别常见的异常类(如 NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException、ClassCastException)”

ME-Q1)

给定以下AnimalLionJumpable类的定义,选择不会导致编译错误或运行时异常的变量赋值组合(选择 2 个选项)。

interface Jumpable {}
class Animal {}
class Lion extends Animal implements Jumpable {}
  1. Jumpable var1 = new Jumpable();
  2. Animal var2 = new Animal();
  3. Lion var3 = new Animal();
  4. Jumpable var4 = new Animal();
  5. Jumpable var5 = new Lion();
  6. Jumpable var6 = (Jumpable)(new Animal());

答案:b, e

解释:选项(a)是错误的。一个interface不能被实例化。

选项(c)是错误的。派生类的引用变量不能用来引用其基类的对象。

选项(d)是错误的。类型为Jumpable的引用变量不能用来引用Animal类的对象,因为Animal没有实现Jumpable接口。

选项(f)是错误的。尽管此行代码可以成功编译,但在运行时将抛出ClassCastException。你可以显式地将任何对象转换为接口,即使它没有实现该接口以使代码编译。但如果对象的类没有实现该接口,代码将在运行时抛出ClassCastException

[8.2] 创建一个 try-catch 块并确定异常如何改变正常程序流程

ME-Q2)

给定以下代码,如果用以下选项替换/*INSERT CODE HERE*/,将使代码打印1?(选择 1 个选项。)

try {
    String[][] names = {{"Andre", "Mike"}, null, {"Pedro"}};
    System.out.println (names[2][1].substring(0, 2));
} catch (/*INSERT CODE HERE*/) {
    System.out.println(1);
}
  1. IndexPositionException e
  2. NullPointerException e
  3. ArrayIndexOutOfBoundsException e
  4. ArrayOutOfBoundsException e

答案:c

解释:选项(a)和(d)是错误的,因为 Java API 没有定义任何具有这些名称的异常类。

这里是本问题中代码初始化的数组值列表:

names[0][0] = "Andre"
names[0][1] = "Mike"
names[1] = null
names[2][0] = "Pedro"

因为数组位置[2][1]没有定义,任何尝试访问它的操作都将抛出ArrayIndexOutOfBoundsException

尝试访问第二个数组的任何位置(即names[1][0])将抛出NullPointerException,因为names[1]被设置为null

[8.2] 创建一个 try-catch 块并确定异常如何改变正常程序流程

ME-Q3)

以下代码的输出是什么?(选择 1 个选项。)

public static void main(String[] args) {
    int a = 10; String name = null;
    try {
        a = name.length();                   //line1
        a++;                                 //line2
    } catch (NullPointerException e){
        ++a;
        return;
    } catch (RuntimeException e){
        a--;
        return;
    } finally {
        System.out.println(a);
    }
}
  1. 5
  2. 6
  3. 10
  4. 11
  5. 12
  6. 编译错误
  7. 无输出
  8. 运行时异常

答案:d

解释:因为变量name没有被赋值,所以不能使用它调用实例方法(length())。以下代码行将抛出NullPointerException

name.length();

当抛出异常时,控制权转移到异常处理器,跳过了try块中剩余代码的执行。因此,在标记为 line2 的注释处的代码(a++)没有执行。

该代码为NullPointerExceptionRuntimeException定义了异常处理器。当抛出异常时,不会执行多个异常处理器。在这种情况下,由于它更具体且定义在RuntimeException之前,NullPointerException的异常处理器将执行。NullPointerException的异常处理器包括以下代码:

++a;
return;

上述代码将变量a的值增加 1;在它退出main方法之前,由于调用了return语句,它执行了finally块,输出了值11。即使catch块中包含return语句,finally块也会执行。

[3.4] 使用 switch 语句

ME-Q4

给定以下类定义,

class Student { int marks = 10; }

以下代码的输出是什么?(选择 1 个选项。)

class Result {
    public static void main(String... args) {
        Student s = new Student();
        switch (s.marks) {
            default: System.out.println("100");
            case 10: System.out.println("10");
            case 98: System.out.println("98");
        }
    }
}
  1. 100
    10
    98
    
  2. 图片

  3. 10
    98
    
  4. 100
    
  5. 10
    

答案:b

说明:只有当没有找到匹配值时,default情况才会执行。在这种情况下,找到了匹配值10,并打印了case标签。因为break语句没有终止此case标签,代码执行将继续并执行switch块内的剩余语句,直到break语句终止它或它结束。

[7.4] 使用 super 和 this 来访问对象和构造函数

ME-Q5

给定以下代码,哪个代码可以用来创建并初始化ColorPencil类的对象?(选择 2 个选项。)

class Pencil {}
class ColorPencil extends Pencil {
    String color;
    ColorPencil(String color) {this.color = color;}
}
  1. ColorPencil var1 = new ColorPencil();
  2. ColorPencil var2 = new ColorPencil(RED);
  3. 图片 ColorPencil var3 = new ColorPencil("RED");
  4. 图片 Pencil var4 = new ColorPencil("BLUE");

答案:c, d

说明:选项(a)是不正确的,因为new ColorPencil()试图调用ColorPencil类的无参构造函数,而这个构造函数在ColorPencil类中未定义。

选项(b)是不正确的,因为new ColorPencil(RED)试图传递一个未在代码中定义的变量RED

[2.3] 了解如何读取或写入对象字段

ME-Q6

以下代码的输出是什么?(选择 1 个选项。)

class Doctor {
    protected int age;
    protected void setAge(int val) { age = val; }
    protected int getAge() { return age; }
}
class Surgeon extends Doctor {
    Surgeon(String val) {
        specialization = val;
    }
    String specialization;
    String getSpecialization() { return specialization; }
}
class Hospital {
    public static void main(String args[]) {
        Surgeon s1 = new Surgeon("Liver");

        Surgeon s2 = new Surgeon("Heart");
        s1.age = 45;
        System.out.println(s1.age + s2.getSpecialization());
        System.out.println(s2.age + s1.getSpecialization());
    }
}
  1. 图片

  2. 45Heart
    0Liver
    
  3. 45Liver
    0Heart
    
  4. 45Liver
    45Heart
    
  5. 45Heart
    45Heart
    
  6. 类无法编译。

答案:a

解释:类 Surgeon 的构造函数将 "Liver""Heart" 的值赋给对象 s1s2specialization 变量。变量 age 在类 Doctor 中是 protected- 的。此外,类 Surgeon 继承了类 Doctor。因此,变量 age 可以被引用变量 s1s2 访问。代码将 45 的值赋给引用变量 s1 的成员变量 age。引用变量 s2age 变量被初始化为 int 的默认值,即 0。因此,代码打印了选项 (a) 中提到的值。

[3.1] 使用 Java 运算符;包括括号以覆盖运算符优先级

ME-Q7)

以下代码的输出是什么?(选择 1 个选项。)

class RocketScience {
    public static void main(String args[]) {
        int a = 0;
        while (a == a++) {
            a++;
            System.out.println(a);
        }
    }
}
  1. while 循环不会执行;什么也不会打印。
  2. while 循环将无限执行,从 1 开始打印所有数字。
  3. while 循环将无限执行,从 0 开始打印所有偶数。
  4. while 循环将无限执行,从 2 开始打印所有偶数。
  5. while 循环将无限执行,从 1 开始打印所有奇数。
  6. while 循环将无限执行,从 3 开始打印所有奇数。

答案:d

解释:while 循环将无限执行,因为条件 a == a++ 总是评估为 true。后缀一元运算符将在用于比较表达式后增加变量的值。循环体中的 a++ 将变量的值增加 1。因此,在单个循环中,a 的值增加 2

[1.4] 在你的代码中导入其他 Java 包以使其可访问

ME-Q8)

给定以下语句,

  • com.ejava 是一个包
  • 在包 com.ejava 中定义了 Person
  • 在包 com.ejava 中定义了 Course

以下哪个选项正确地导入了类 PersonCourse 到类 MyEJava 中?(选择 3 个选项。)

  1. import com.ejava.*;
    class MyEJava {}
    
  2. import com.ejava;
    class MyEJava {}
    
  3. import com.ejava.Person;
    import com.ejava.Course;
    class MyEJava {}
    
  4. import com.ejava.Person;
    import com.ejava.*;
    class MyEJava {}
    

答案:a, c, d

解释:选项 (a) 是正确的。语句 import com.ejava.*; 导入了类 MyEJava 中包 com.ejava 的所有公共成员。

选项 (b) 是错误的。因为 com.ejava 是一个包,要导入该包中定义的所有类,应该在包名后跟 .*

import com.ejava.*;

选项 (c) 是正确的。它使用两个单独的 import 语句分别导入类 PersonCourse,这是正确的。

选项 (d) 也是正确的。第一个 import 语句仅导入 MyClass 中的 Person 类。但第二个 import 语句从 com.ejava 包中导入了 PersonCourse 类。在 Java 类中,你可以多次导入同一个类而不会出现问题。此代码是正确的。

在 Java 中,import 语句使导入的类对 Java 编译器可见,允许导入它的类引用它。在 Java 中,import 语句不会将导入的类嵌入到目标类中。

[6.4] 应用访问修饰符

ME-Q9)

假设以下类 AnimalForest 定义在同一个包中,检查代码并选择正确的语句(选择 2 个选项)。

line1>    class Animal {
line2>        public void printKing() {
line3>            System.out.println("Lion");
line4>        }
line5>    }

line6>    class Forest {
line7>        public static void main(String... args) {
line8>            Animal anAnimal = new Animal();
line9>            anAnimal.printKing();
line10>        }
line11>    }
  1. Forest 打印 Lion

  2. 如果将第 2 行的代码更改为如下所示,类 Forest 将打印 Lion

  3. private void printKing() {
    
  4. 如果将第 2 行的代码更改为如下所示,类 Forest 将打印 Lion:

  5. void printKing() {
    
  6. 如果将第 2 行的代码更改为如下所示,类 Forest 将打印 Lion

  7. default void printKing() {
    

答案:a, c

说明:选项 (a) 是正确的。代码将成功编译并打印 Lion

选项 (b) 是错误的。如果将方法 printKing 的访问修饰符更改为 private,则代码将无法编译。类中的 private 成员不能在类外访问。

选项 (c) 是正确的。类 AnimalForest 定义在同一个包中,因此将方法 printKing 的访问修饰符更改为默认访问权限仍然使其在类 Forest 中可访问。该类将成功编译并打印 Lion

选项 (d) 是错误的。“default” 不是 Java 中的有效访问修饰符或关键字。在 Java 中,默认访问权限通过没有任何显式访问修饰符来标记。此代码将无法编译。

[1.3] 使用主方法创建可执行的 Java 应用程序;从命令行运行 Java 程序;包括控制台输出。

ME-Q10)

给定以下代码,

class MainMethod {
    public static void main(String... args) {
        System.out.println(args[0]+":"+ args[2]);
    }
}

如果使用以下命令执行,它的输出是什么?(选择 1 个选项。)

java MainMethod 1+2 2*3 4-3 5+1
  1. java:1+2
  2. java:3
  3. MainMethod:2*3
  4. MainMethod:6
  5. 1+2:2*3
  6. 3:3
  7. 6
  8. 1+2:4-3
  9. 31
  10. 4

答案:h

说明:此问题测试了多个知识点。

  1. 传递给主方法的参数——关键字 java 和类的名称 (MainMethod) 并没有作为参数传递给 main 方法。跟在类名称后面的参数传递给 main 方法。在这种情况下,将四个方法参数传递给 main 方法,如下所示:

  2. args[0]: 1+2
    args[1]: 2*3
    args[2]: 4-3
    args[3]: 5+1
    
  3. 传递给主方法的参数类型—主方法接受类型为 String 的参数。所有数值表达式—1+22*35+14-3—都作为字面 String 值传递。当你尝试打印它们的值时,这些表达式不会进行评估。因此,args[0] 不会打印为 3,而是打印为 1+2

  4. String 数组元素进行 + 操作—因为传递给 main 方法的数组包含所有 String 值,使用 + 运算符与单个值一起使用将连接其值。如果它们是数值表达式,则不会将值相加。因此,"1+2"+"4-3" 不会评估为 314

[2.2] 区分对象引用变量和原始变量

[9.5] 编写一个简单的 Lambda 表达式,该表达式消费一个 Lambda 断言表达式

ME-Q11)

以下代码的输出是什么?(选择 1 个选项。)

interface Moveable {
    int move(int distance);
}
class Person {
    static int MIN_DISTANCE = 5;
    int age;
    float height;
    boolean result;
    String name;
}
public class EJava {
    public static void main(String arguments[]) {
        Person person = new Person();
        Moveable moveable = (x) -> Person.MIN_DISTANCE + x;
        System.out.println(person.name + person.height + person.result
                                       + person.age + moveable.move(20));
    }
}
  1. null0.0false025
  2. null0false025
  3. null0.0ffalse025
  4. 0.0false025
  5. 0false025
  6. 0.0ffalse025
  7. null0.0true025
  8. 0true025
  9. 0.0ftrue025
  10. 编译错误
  11. 运行时异常

答案:a

说明:如果未显式分配值,则类的实例变量都将分配默认值。以下是原始数据类型和对象的默认值:

  • char -> \u0000
  • byte, short, int -> 0
  • long -> 0L
  • float -> 0.0f
  • double -> 0.0d
  • boolean -> false
  • 对象 -> null

Moveable 是一个函数式接口。示例代码通过使用 Lambda 表达式 (x) -> Person.MIN _DISTANCE + x 定义了其函数式方法 move 的执行代码。

调用 moveable.move(20)20 作为参数传递给 move 方法。它返回 25(Person.MIN_DISTANCE 的和,即 5,加上方法参数 20)。

[7.3] 确定何时需要类型转换

ME-Q12)

给定以下代码,以下哪个选项,如果用于替换 /* INSERT CODE HERE */,将使代码打印变量 pagesPerMin 的值?(选择 1 个选项。)

class Printer {
    int inkLevel;
}
class LaserPrinter extends Printer {
    int pagesPerMin;
    public static void main(String args[]) {
        Printer myPrinter = new LaserPrinter();
        System.out.println(/* INSERT CODE HERE */);
    }
}
  1. (LaserPrinter)myPrinter.pagesPerMin
  2. myPrinter.pagesPerMin
  3. LaserPrinter.myPrinter.pagesPerMin
  4. ((LaserPrinter)myPrinter).pagesPerMin

答案:d

说明:选项 (a) 是错误的,因为 (LaserPrinter) 尝试将 myPrinter.pagesPerMin(原始类型 int 的变量)转换为 LaserPrinter,这是不正确的。此代码无法编译。

选项 (b) 是错误的。引用变量 myPrinter 的类型是 PrintermyPrinter 引用了 LaserPrinter 类的对象,该类扩展了 Printer 类。基类引用变量不能在没有显式转换的情况下访问其子类中定义的变量和方法。

选项 (c) 是不正确的。LaserPrinter.myPrinterLaserPrinter 视为一个变量,尽管在问题的代码中不存在具有此名称的变量。此代码无法编译。

[2.1] 声明和初始化变量(包括原始数据类型的转换)

[9.5] 编写一个简单的 Lambda 表达式,它消费一个 Lambda 断言表达式

ME-Q13)

以下代码的输出是什么?(选择一个选项。)

interface Keys {
    String keypad(String region, int keys);
}
public class Handset {
    public static void main(String... args) {
        double price;
        String model;
        Keys varKeys = (region, keys) ->
                       {if (keys >= 32)
                        return region; else return "default";};
        System.out.println(model + price + varKeys.keypad("AB", 32));
    }
}
  1. null0AB
  2. null0.0AB
  3. null0default
  4. null0.0default
  5. 0
  6. 0.0
  7. 编译错误

答案:g

解释:局部变量(在方法内部声明的变量)不会使用它们的默认值进行初始化。如果你在初始化之前尝试打印局部变量的值,代码将无法编译。

代码中使用的 Lambda 表达式是正确的。

[3.1] 使用 Java 运算符;包括括号来覆盖运算符优先级

ME-Q14)

以下代码的输出是什么?(选择一个选项。)

public class Sales {
    public static void main(String args[]) {
        int salesPhone = 1;
        System.out.println(salesPhone++ + ++salesPhone +
                                                       ++salesPhone);
    }
}
  1. 5
  2. 6
  3. 8
  4. 9

答案:c

解释:理解以下规则将使你能够正确回答这个问题:

  • 算术表达式是从左到右进行评估的。
  • 当一个表达式使用后缀一元增量运算符 (++) 时,它的值在用于表达式之后增加。
  • 当一个表达式使用前缀一元增量运算符 (++) 时,它的值在用于表达式之前增加。

变量 salesPhone 的初始值是 1。让我们逐步评估算术表达式 salesPhone++ + ++salesPhone + ++salesPhone 的结果:

  1. salesPhone 的第一次出现使用后缀表示法中的 ++,因此它的值在增加 1 前被用于表达式。这意味着表达式的结果是

  2. 1 + ++salesPhone + ++salesPhone
    
  3. 注意,之前使用后缀增量运算符 ++ 已经将 salesPhone 的值增加到 2salesPhone 的第二次出现使用前缀表示法中的 ++,因此它的值在增加 1 后被用于表达式,值为 3。这意味着表达式的结果是

  4. 1 + 3 + ++salesPhone
    
  5. salesPhone 的第三次出现再次使用前缀表示法中的 ++,因此它的值在增加 1 后被用于表达式,值为 4。这意味着表达式的结果是

  6. 1 + 3 + 4
    

    前面的表达式计算结果为 8。

[1.2] 定义 Java 类的结构

[1.4] 导入其他 Java 包以使它们在代码中可访问

ME-Q15)

以下哪个选项定义了编译成功的 Java 类的正确结构?(选择一个选项。)

  1. package com.ejava.guru;
    package com.ejava.oracle;
    class MyClass {
        int age = /* 25 */ 74;
    }
    
  2. import com.ejava.guru.*;
    import com.ejava.oracle.*;
    package com.ejava;
    class MyClass {
        String name = "e" + "Ja /*va*/ v";
    }
    
  3. class MyClass {
        import com.ejava.guru.*;
    }
    
  4. class MyClass {
        int abc;
        String course = //this is a comment
                     "eJava";
    }
    
  5. 以上皆非

答案:d

说明:这个问题需要你了解

  • 正确的注释语法和用法
  • importpackage 语句的使用

没有代码由于行尾或多行注释而无法编译。以下所有代码行都是有效的:

int age = /* 25 */ 74;
String name = "e" + "Ja /*va*/ v";
String course = //this is a comment
                 "eJava";

在前面的代码中,变量 age 被分配了整数值 74,变量 name 被分配了字符串值 "eJa /*va*/ v",而 course 被分配了字符串值 "eJava"。如果多行注释分隔符放在字符串定义内部,则会被忽略。让我们看看所有选项在 packageimport 语句使用上的表现:

选项 (a) 是错误的。一个类不能定义多个 package 语句。

选项 (b) 是错误的。尽管一个类可以在类中导入多个包,但 package 语句必须放在 import 语句之前。

选项 (c) 是错误的。一个类不能在其类体内部定义 import 语句。import 语句出现在类体之前。

选项 (d) 是正确的。在没有任何包信息的情况下,此类成为默认包的一部分。

[3.1] 使用 Java 运算符;包括括号以覆盖运算符优先级

ME-Q16)

以下代码的输出是什么?(选择 1 个选项。)

class OpPre {
    public static void main(String... args) {
        int x = 10;
        int y = 20;
        int z = 30;
        if (x+y%z > (x+(-y)*(-z))) {
            System.out.println(x + y + z);
        }
    }
}
  1. 60
  2. 59
  3. 61
  4. 无输出。
  5. 代码无法编译。

答案:d

说明:x+y%z 计算结果为 30(x+(y%z))(x+(-y)*(-z)) 计算结果为 610if 条件返回 false,打印 xyz 之和的代码行不执行。因此,代码没有提供任何输出。

[1.1] 定义变量的作用域

[6.2] 将 static 关键字应用于方法和字段

ME-Q17)

选择变量 name 的最合适的定义以及它应该声明的行号,以便以下代码成功编译(选择 1 个选项)。

class EJava {
    // LINE 1
    public EJava() {
        System.out.println(name);
    }
    void calc() {
        // LINE 2
        if (8 > 2) {
            System.out.println(name);
        }
    }
    public static void main(String... args) {
        // LINE 3
        System.out.println(name);
    }
}
  1. 在第 1 行定义 static String name;
  2. 在第 1 行定义 String name;
  3. 在第 2 行定义 String name;
  4. 在第 3 行定义 String name;

答案:a

说明:变量 name 必须在实例方法 calc、类构造函数和 static 方法 main 中可访问。非 static 变量不能被 static 方法访问。因此,唯一合适的选项是定义一个 static 变量 name,它可以被所有者访问:类 EJava 的构造函数以及方法 calcmain

[2.4] 解释一个对象的生命周期(创建、通过重新赋值进行“解引用”和垃圾回收)

ME-Q18)

检查以下代码并选择正确的陈述(选择 1 个选项)。

line1>    class Emp {
line2>        Emp mgr = new Emp();
line3>    }

line4>    class Office {
line5>        public static void main(String args[]) {
line6>            Emp e = null;
line7>            e = new Emp();
line8>            e = null;
line9>        }
line10>    }
  1. 在第 8 行,对象 e 指代的对象符合垃圾回收的条件。
  2. 在第 9 行,对象 e 指向的对象符合垃圾收集的条件。
  3. 由于对象 e 的成员变量 mgr 没有被设置为 null,因此对象 e 指向的对象不符合垃圾收集的条件。
  4. 代码抛出运行时异常,代码执行永远不会到达第 8 行或第 9 行。

答案:d

说明:代码在运行时抛出 java.lang.StackOverflowError。第 7 行创建了一个 Emp 类的实例。创建 Emp 类的对象需要创建一个实例变量 mgr 并将其初始化为同一类的对象。正如你所看到的,Emp 对象的创建会递归地调用自身(没有退出条件),导致 java.lang.StackOverflowError

[2.5] 开发使用包装类(如 Boolean、Double 和 Integer)的代码。

[6.1] 创建具有参数和返回值的方法;包括重载方法

ME-Q19

给定以下,

long result;

哪些选项是接受两个 String 参数和一个 int 参数的方法的正确声明,并且其返回值可以被分配给变量 result?(选择 3 个选项。)

  1. Short myMethod1(String str1, int str2, String str3)
  2. Int myMethod2(String val1, int val2, String val3)
  3. Byte myMethod3(String str1, str2, int a)
  4. Float myMethod4(String val1, val2, int val3)
  5. Long myMethod5(int str2, String str3, String str1)
  6. Long myMethod6(String... val1, int val2)
  7. Short myMethod7(int val1, String... val2)

答案:a, e, g

说明:方法参数的类型和方法参数的名称的位置无关紧要。你可以接受两个 String 变量,然后是一个 int 变量,或者是一个 String 变量后跟 int,然后再是一个 Stringint 变量的名称可以是 str2。只要名称是有效的标识符,任何名称都是可接受的。方法返回类型必须可以被分配给类型为 long 的变量。

选项(a)是正确的。Short 实例的值可以被分配给原始类型 long 的变量。

选项(b)是错误的。它无法编译,因为 Int 没有在 Java API 中定义。int 数据类型的正确包装类名是 Integer

选项(c)和(d)是错误的。与多个变量的声明不同,这些声明可以由它们的数据类型的一次出现 precede,每个方法参数必须由其类型 precede。以下变量的声明是有效的:

int aa, bb;

但以下声明中方法参数的声明不是:

Byte myMethod3(String str1, str2, int a) { /*code*/ }

选项(e)是正确的。Long 实例的值可以被分配给原始类型 long 的变量。

选项(f)无法编译。如果使用 varargs 定义方法参数,它必须是最后一个。

选项 (g) 是正确的。方法参数 val2,一个可变参数,可以接受两个 String 参数。此外,method7() 方法的返回值,一个 Short 类型,可以被赋值给 long 类型的变量。

[4.1] 声明、实例化、初始化和使用一维数组

ME-Q20)

以下哪个选项可以成功编译?(选择 3 个选项。)

  1. int eArr1[] = {10, 23, 10, 2};
  2. int[] eArr2 = new int[10];
  3. int[] eArr3 = new int[] {};
  4. int[] eArr4 = new int[10] {};
  5. int eArr5[] = new int[2] {10, 20};

答案:a, b, c

说明:选项 (d) 是不正确的,因为它在 {} 中定义了数组的大小,这是不允许的。以下两行代码都是正确的:

int[] eArr4 = new int[10];
int[] eArr4 = new int[]{};

选项 (e) 是不正确的,因为在单行代码中声明、实例化和初始化数组时,不能在方括号中指定数组的大小。

[6.1] 创建具有参数和返回值的方法;包括重载方法

[9.2] 创建并操作字符串

ME-Q21)

假设 Oracle 要求您创建一个返回两个 String 对象连接值的 String 方法。以下哪个方法可以完成这项工作?(选择 2 个选项。)

  1. public String add(String 1, String 2) {
        return str1 + str2;
    }
    
  2. private String add(String s1, String s2) {
        return s1.concat(s2);
    }
    
  3. protected String add(String value1, String value2) {
        return value2.append(value2);
    }
    
  4. String subtract(String first, String second) {
        return first.concat(second.substring(0));
    }
    

答案:b, d

说明:选项 (a) 是不正确的。此方法使用无效的标识符名称定义了方法参数。标识符不能以数字开头。

选项 (b) 是正确的。方法要求没有提到所需方法的访问修饰符。它可以有任何可访问性。

选项 (c) 是不正确的,因为 String 类没有定义任何 append 方法。

选项 (d) 是正确的。尽管方法的名称——subtract——不是一个试图连接两个值的方法的合适名称,但它确实完成了所需的工作。

[3.3] 创建 if 和 if/else 以及三元构造

[5.2] 创建并使用 for 循环,包括增强型 for 循环

[5.5] 使用 break 和 continue

ME-Q22)

给定以下内容,

int ctr = 10;
char[] arrC1 = new char[]{'P','a','u','l'};
char[] arrC2 = {'H','a','r','r','y'};
//INSERT CODE HERE
System.out.println(ctr);

//INSERT CODE HERE, 插入哪些选项将输出 14?(选择 2 个选项。)

  1. for (char c1 : arrC1) {
        for (char c2 : arrC2) {
            if (c2 == 'a') break;
            ++ctr;
        }
    }
    
  2. for (char c1 : arrC1)
        for (char c2 : arrC2) {
            if (c2 == 'a') break;
            ++ctr;
        }
    
  3. for (char c1 : arrC1)
        for (char c2 : arrC2)
            if (c2 == 'a') break;
            ++ctr;
    
  4. for (char c1 : arrC1) {
        for (char c2 : arrC2) {
            if (c2 == 'a') continue;
            ++ctr;
        }
    }
    

答案:a, b

说明:选项 (a) 和 (b) 只在外部 for 构造中使用 {} 的用法上有所不同。您可以使用 {} 将要执行的迭代构造(如 dodo-whilefor)中的语句分组。{} 也与条件构造(如 switchif-else)一起使用。

变量 ctr 的初始值为 10。数组 arrC1 的大小为 4,数组 arrC2 的大小为 5,且在第二个位置上为 'a'。外循环执行四次。因为 arrC2 指向的第二个字符是 'a',内循环将变量 ctr 的值增加其第一个元素。内循环不会对第二个元素执行 ++ctr,因为 c2=='a' 返回 true 并执行 break 语句,从而退出内循环。内循环将 ctr 的值增加四次,使其值增加到 14

选项 (c) 是错误的。因为内层 for 循环没有使用 {} 将必须执行的代码行分组,所以 ++ctr 不是内层 for 循环的一部分。

选项 (d) 是错误的。代码 ++ctr 只在完成外层和内层 for 循环后执行一次,因为它不是循环结构的一部分。

[6.1] 创建具有参数和返回值的方法;包括重载方法

ME-Q23)

给定以下 ChemistryBook 类的定义,选择正确的单个语句(选择 2 个选项):

import java.util.ArrayList;
class ChemistryBook {
    public void read() {}                //METHOD1
    public String read() { return null; }     //METHOD2
    ArrayList read(int a) { return null; }     //METHOD3
}
  1. 标记为 //METHOD1//METHOD2 的方法是正确重载的方法。
  2. 标记为 //METHOD2//METHOD3 的方法是正确重载的方法。
  3. 标记为 //METHOD1//METHOD3 的方法是正确重载的方法。
  4. 所有方法——标记为 //METHOD1//METHOD2//METHOD3 的方法——都是正确重载的方法。

答案:b, c

解释:选项 (a) 和 (d) 是错误的,因为标记为 //METHOD1//METHOD2read 方法仅在返回类型上有所不同,voidString。重载方法不能仅通过改变它们的返回类型来定义;因此,这些方法不符合正确重载方法的条件。

注意,标记为 //METHOD1//METHOD2 的方法同时存在将导致编译错误。

[6.2] 将静态关键字应用于方法和字段

[6.3] 创建和重载构造函数;包括对默认构造函数的影响

[6.4] 应用访问修饰符

ME-Q24)

给定以下内容,

final class Home {
    String name;
    int rooms;
    //INSERT CONSTRUCTOR HERE
}

//INSERT CONSTRUCTOR HERE 插入哪些选项将定义有效的重载构造函数为 Home 类?(选择 3 个选项。)

  1. Home() {}
  2. Float Home() {}
  3. protected Home(int rooms) {}
  4. final Home() {}
  5. private Home(long name) {}
  6. float Home(int rooms, String name) {}
  7. static Home() {}

答案:a, c, e

说明:构造函数不能定义显式的返回类型。如果你用它来这样做,它就不再是构造函数了。构造函数可以使用任何访问级别定义——私有、默认、受保护和公共——无论用于声明类的访问级别如何。

选项 (b) 和 (f) 是错误的,因为它们定义了显式的返回类型:Floatfloat。这些选项中的代码定义了一个名为 Home 的方法,而不是构造函数。

选项 (d) 和 (g) 是错误的。代码无法编译,因为构造函数不能使用非访问修饰符 staticabstractfinal 定义。

[3.3] 创建 if 和 if/else 以及三元构造

[5.2] 创建和使用 for 循环(包括增强型 for 循环)

[5.5] 使用 break 和 continue

ME-Q25)

给定以下代码,以下哪个选项,如果用于替换 //INSERT CODE HERE,将使代码打印出完全能被 14 整除的数字?(选择一个选项。)

for (int ctr = 2; ctr <= 30; ++ctr) {
    if (ctr % 7 != 0)
        //INSERT CODE HERE
    if (ctr % 14 == 0)
        System.out.println(ctr);
}
  1. continue;
  2. exit;
  3. break;
  4. end;

答案:a

说明:选项 (b) 和 (d) 是错误的,因为 exitend 在 Java 中不是有效的语句。

选项 (c) 是错误的。使用 break 将在 for 循环的第一个迭代中终止循环,因此不会打印任何输出。

[2.1] 声明和初始化变量(包括原始数据类型的转换)

[9.5] 编写一个简单的 Lambda 表达式,它消费一个 Lambda 断言表达式

ME-Q26)

以下代码的输出是什么?(选择一个选项。)

import java.util.function.Predicate;
public class MyCalendar {
    public static void main(String arguments[]) {
        Season season1 = new Season();
        season1.name = "Spring";

        Season season2 = new Season();
        season2.name = "Autumn";

        Predicate<String> aSeason = (s) -> s == "Summer" ?
                                season1.name : season2.name;

        season1 = season2;
        System.out.println(season1.name);
        System.out.println(season2.name);
        System.out.println(aSeason.test(new String("Summer")));
    }
}
class Season {
    String name;
}
  1. String
    Autumn
    false
    
  2. Spring
    String
    false
    
  3. Autumn
    Autumn
    false
    
  4. Autumn
    String
    true
    
  5. 编译错误
  6. 运行时异常

答案:e

说明:函数式接口 Predicate 中的函数方法 test 的返回类型是 boolean。以下 Lambda 表达式试图返回一个 String 值,因此代码无法编译:

Predicate<String> aSeason = (s) -> s == "Summer" ?
                           season1.name : season2.name;

此问题还涵盖了另一个重要主题:多个变量引用可以引用相同的实例。假设你将前面的 Lambda 表达式修改如下:

Predicate<String> aSeason = (s) -> s == "Summer";

在这种情况下,代码将输出以下内容:

Autumn
Autumn
false

这是因为多个变量引用可以指向同一个对象。以下代码行定义了一个引用变量 season1,它引用了一个对象,其实例变量(name)的值被设置为 Spring

Season season1 = new Season();
season1.name = "Spring";

以下代码行定义了一个引用变量 season2,它引用了一个对象,其实例变量(name)的值被设置为 Autumn

Season season2 = new Season();
season2.name = "Autumn";

以下代码行重新初始化引用变量 season1 并将其赋值给由变量 season2 指引用的对象:

season1 = season2;

现在,变量 season1 指向的对象也由变量 season2 指向。这两个变量都指向同一个对象——该对象的实例变量值被设置为 Autumn。因此,修改后的代码的输出如下:

Autumn
Autumn
false

快速回顾一下前述输出的原因:使用 new 操作符创建的值为 "Summer"String 对象永远不会被 JVM 池化。在这里,这样的实例通过 == 操作符与池化的 "Summer" 实例进行比较。

[6.3] 创建和重载构造函数;包括对默认构造函数的影响

ME-Q27)

以下代码的哪个说法是正确的?(选择 1 个选项。)

class Shoe {}
class Boot extends Shoe {}
class ShoeFactory {
    ShoeFactory(Boot val) {
        System.out.println("boot");
    }
    ShoeFactory(Shoe val) {
        System.out.println("shoe");
    }
}
  1. ShoeFactory 类总共有两个重载构造函数。
  2. ShoeFactory 类有三个重载构造函数,两个用户定义的构造函数和一个默认构造函数。
  3. ShoeFactory 类将无法编译。
  4. 添加以下构造函数将使 ShoeFactory 类的构造函数数量增加到 3 个:
  5. private ShoeFactory (Shoe arg) {}
    

答案:a

说明:Java 接受基类派生类对象的变化作为定义重载构造函数和方法的唯一标准。

选项 (b) 是错误的,因为 Java 不会为已经定义了构造函数的类生成默认构造函数。

选项 (c) 是错误的。为这个示例定义的所有类都成功编译。

选项 (d) 是错误的。ShoeFactory 类已经定义了一个接受类型为 Shoe 的方法参数的构造函数。你不能仅仅通过改变其访问修饰符来重载构造函数。

[7.4] 使用 super 和 this 访问对象和构造函数

ME-Q28)

给定以下 ColorPencilTestColor 类的定义,哪个选项如果用于替换 //INSERT CODE HERE,将初始化引用变量 myPencil 的实例变量 color 为字符串字面值 "RED"?(选择 1 个选项。)

class ColorPencil {
    String color;
    ColorPencil(String color) {
        //INSERT CODE HERE
    }
}
class TestColor {
    ColorPencil myPencil = new ColorPencil("RED");
}
  1. this.color = color;
  2. color = color;
  3. color = RED;
  4. this.color = RED;

答案:a

说明:选项 (b) 是错误的。此行代码将方法参数的值赋给自己。ColorPencil 类的构造函数定义了一个与其实例变量同名的方法参数,color。在构造函数中访问实例变量时,必须使用关键字 this 前缀,否则它将引用方法参数 color

选项 (c) 和 (d) 是错误的。它们试图访问未在代码中定义的变量 RED 的值。

[2.3] 了解如何读取或写入对象字段

ME-Q29)

以下代码的输出是什么?(选择 1 个选项。)

class EJavaCourse {
    String courseName = "Java";
}
class University {
    public static void main(String args[]) {
        EJavaCourse courses[] = { new EJavaCourse(), new EJavaCourse() };
        courses[0].courseName = "OCA";
        for (EJavaCourse c : courses) c = new EJavaCourse();
        for (EJavaCourse c : courses) System.out.println(c.courseName);
    }
}
  1. Java
    Java
    
  2. OCA
    Java
    
  3. OCA
    OCA
    
  4. 以上皆非

答案:b

说明:这个问题测试了您对多个概念的理解:如何从对象字段中读取和写入,如何使用数组,增强型for循环,以及将值分配给循环变量。

代码定义了一个包含两个元素的EJavaCourse类数组。变量courseName的默认值Java被分配给这两个元素中的每一个。courses[0].courseName = "OCA"改变了存储在数组位置0的对象的courseName值。c = new EJavaCourse()将一个新的对象分配给循环变量c。这个赋值不会重新分配新对象给数组引用变量。System.out.println(c.courseName)使用循环变量c打印最初由数组存储的courseName对象的名称。

增强型for循环中的循环变量指向数组或列表元素的副本。如果您修改循环变量的状态,修改后的对象状态将在数组中反映出来。但如果您将新对象分配给循环变量,则它不会反映在正在迭代的列表或数组中。您可以将增强型for循环变量的这种行为与将对象引用作为参数传递给方法的这种行为进行比较。

[6.2] 将static关键字应用于方法和字段

[7.2] 开发演示多态使用的代码;包括重写和对象类型与引用类型

ME-Q30)

以下代码的输出是什么?(选择 1 个选项。)

class Phone {
    static void call() {
        System.out.println("Call-Phone");
    }
}
class SmartPhone extends Phone{
    static void call() {
        System.out.println("Call-SmartPhone");
    }
}
class TestPhones {
    public static void main(String... args) {
        Phone phone = new Phone();
        Phone smartPhone = new SmartPhone();
        phone.call();
        smartPhone.call();
    }
}
  1. common7.jpg

  2. Call-Phone
    Call-Phone
    
  3. Call-Phone
    Call-SmartPhone
    
  4. Call-Phone
    null
    
  5. null
    Call-SmartPhone
    

答案:a

说明:静态方法的调用与引用变量的类型相关联,不依赖于分配给引用变量的对象的类型。静态方法属于类,而不是属于其对象。重新审视以下代码:

Phone smartPhone = new SmartPhone();
smartPhone.call();

在前面的代码中,引用变量smartPhone的类型是Phone。因为call是一个static方法,所以smartPhone.call()调用在Phone类中定义的call方法。

[8.1] 区分检查型异常、非检查型异常和错误

ME-Q31)

给定以下代码,以下哪些陈述是正确的?(选择 3 个选项。)

class MyExam {
    void question() {
        try {
            question();
        } catch (StackOverflowError e) {
            System.out.println("caught");
        }
    }
    public static void main(String args[]) {
        new MyExam().question();
    }
}
  1. common6.jpg 代码将打印caught
  2. 代码不会打印caught
  3. common6.jpg 如果StackOverflowError是一个运行时异常,则代码将打印caught
  4. common6.jpg 如果StackOverflowError是一个检查型异常,则代码将打印caught
  5. 如果question()抛出NullPointer-Exception异常,则代码将打印caught

答案:a, c, d

说明:选项(a)是正确的。当遇到StackOverflowError时,控制权将传递到异常处理器。因此,它将打印caught

选项(c)和(d)是正确的。当抛出相应的检查或运行时异常时,异常处理器会执行。

选项(e)是不正确的。StackOverflow类的异常处理器无法处理NullPointerException类的异常,因为NullPointerException不是StackOverflowError的父类。

[6.5] 将封装原则应用于一个类

ME-Q32)

定义如下Student类:

public class Student {
    private String fName;
    private String lName;

    public Student(String first, String last) {
        fName = first; lName = last;
    }
    public String getName() { return fName + lName; }
}

类的创建者后来将getName方法更改如下:

public String getName() {
    return fName + " " + lName;
}

这个变化的含义是什么?(选择 2 个选项。)

  1. 使用Student类的类将无法编译。
  2. 使用Student类的类将无需任何编译问题地工作。
  3. Student类是一个良好封装的类的例子。
  4. Student类将其实例变量暴露在类外部。

答案:b, c

说明:这是一个良好封装的类的例子。修改后,getName方法的签名没有变化。因此,使用此类和方法的任何代码都不会遇到任何编译问题。它的实例变量(fNamelName)没有被暴露在类外部。它们只能通过一个public方法:getName来访问。

[6.2] 将static关键字应用于方法和字段

ME-Q33)

以下代码的输出是什么?(选择 1 个选项。)

class ColorPack {
    int shadeCount = 12;
    static int getShadeCount() {
        return shadeCount;
    }
}
class Artist {
    public static void main(String args[]) {
        ColorPack pack1 = new ColorPack();
        System.out.println(pack1.getShadeCount());
    }
}
  1. 10
  2. 12
  3. 没有输出
  4. 编译错误

答案:d

说明:一个static方法无法访问类的非static实例变量。因此,ColorPack类无法编译。

[6.6] 确定当对象引用和原始值传递给修改它们值的方法时的效果

ME-Q34)

保罗定义了他的LaptopWorkshop类来升级他的笔记本电脑的内存。你认为他成功了么?这段代码的输出是什么?(选择 1 个选项。)

class Laptop {
    String memory = "1 GB";
}
class Workshop {
    public static void main(String args[]) {
        Laptop life = new Laptop();
        repair(life);
        System.out.println(life.memory);
    }
    public static void repair(Laptop laptop) {
        laptop.memory = "2 GB";
    }
}
  1. 1 GB
  2. 2 GB
  3. 编译错误
  4. 运行时异常

答案:b

说明:在这个例子中定义的repair方法修改了传递给它的方法参数laptop的状态。它是通过修改实例变量memory的值来做到这一点的。

当一个方法修改传递给它的对象引用变量的状态时,所做的更改在调用方法中可见。repair方法修改了方法参数laptop的状态;这些更改在main方法中可见。因此,main方法打印出life.memory的值为2 GB

[2.1] 声明和初始化变量(包括原始数据类型的转换)

ME-Q35)

以下代码的输出是什么?(选择 1 个选项。)

public class Application {
    public static void main(String... args) {
        double price = 10;

        String model;
        if (price > 10)
            model = "Smartphone";
        else if (price <= 10)
            model = "landline";
        System.out.println(model);
    }
}
  1. landline
  2. 智能手机
  3. 没有输出
  4. 编译错误

答案:d

解释:局部变量没有使用默认值初始化。尝试打印未初始化局部变量值的代码无法编译。

在此代码中,局部变量model仅被声明,并未初始化。变量model的初始化被放置在ifelse-if结构中。如果你在ifelse-if结构中初始化一个变量,编译器无法确定这些条件是否会评估为true,从而导致局部变量没有初始化。因为没有else在底部,编译器无法判断ifelse-if是否互斥,所以代码无法编译。

如果你从之前的代码中移除条件if (price <= 10),代码将成功编译:

public class Application {
    public static void main(String... args) {
        double price = 10;
        String model;
        if (price > 10)
            model = "Smartphone";
        else
            model = "landline";
        System.out.println(model);
    }
}

在此代码中,编译器可以确定局部变量model的初始化。

[9.2] 创建和操作字符串

ME-Q36)

以下代码的输出是什么?(选择 1 个选项。)

class EString {
    public static void main(String args[]) {
        String eVal = "123456789";

        System.out.println(eVal.substring(eVal.indexOf("2"),
 eVal.indexOf("0")).concat("0"));
    }
}
  1. 234567890
  2. 34567890
  3. 234456789
  4. 3456789
  5. 编译错误
  6. 运行时异常

答案:f

解释:当多个方法在单个代码语句上链式调用时,方法从左到右执行,而不是从右到左。eVal.indexOf("0")返回一个负值,因为正如你所见,String eVal不包含数字0。因此,eVal.substring传递了一个负的end值,这导致运行时异常。

[2.4] 解释对象的生命周期(创建、“通过重新赋值取消引用”和垃圾回收)

ME-Q37)

检查以下代码并选择正确的语句(选择 2 个选项)。

class Artist {
    Artist assistant;
}
class Studio {
    public static void main(String... args) {
        Artist a1 = new Artist();
        Artist a2 = new Artist();
        a2.assistant = a1;
        a2 = null;        // Line 1
    }
     // Line 2
}
  1. 在第 1 行至少有两个对象被垃圾回收。
  2. 在第 1 行至少有一个对象被垃圾回收。
  3. 在第 1 行没有对象被垃圾回收
  4. 在第 1 行被垃圾回收的对象数量是未知的。
  5. 在第 2 行至少有两个对象有资格进行垃圾回收。

答案:d, e

解释:选项(a)、(b)和(c)都是错误的。

当对象引用被标记为null时,对象被标记为垃圾回收。但你不能确定垃圾回收器何时会启动来回收对象。垃圾回收器是一个低优先级的线程,其确切执行时间将取决于操作系统。操作系统会在需要声明未使用空间时启动此线程。你可以确定的是有资格进行垃圾回收的对象数量。你永远不能确定哪些对象已经被垃圾回收,所以任何断言特定数量的对象已经被垃圾回收的陈述都是不正确的。

选项 (d) 是正确的。如前所述,在任何时间点确切地确定垃圾回收的对象数量是不可能的。

选项 (e) 是正确的。如果你标记了这个选项为错误,请重新思考。这个问题是要你选择正确的陈述,这是一个正确的陈述。你可能认为至少有两个对象在第一行已经被标记为垃圾回收,这是正确的。但是因为第二行没有变化,至少还有两个对象仍然符合垃圾回收的条件。

[3.2] 使用 ==equals() 方法测试字符串和其他对象之间的相等性

ME-Q38)

以下代码的输出是什么?(选择 1 个选项。)

class Book {
    String ISBN;
    Book(String val) {
        ISBN = val;
    }
}
class TestEquals {
    public static void main(String... args) {
        Book b1 = new Book("1234-4657");
        Book b2 = new Book("1234-4657");
        System.out.print(b1.equals(b2) +":");
        System.out.print(b1 == b2);
    }
}
  1. true:false
  2. true:true
  3. false:true
  4. false:false
  5. 编译错误——在 Book 类中没有 equals 方法。
  6. 运行时异常

答案:d

说明:比较运算符确定引用变量是否引用同一个对象。因为引用变量 b1b2 引用了不同的对象,所以 b1==b2 打印 false

equals 方法是在 java.lang.Object 类中定义的一个 public 方法。因为 Object 类是 Java 中所有类的超类,所以 equals 方法被所有类继承。因此,代码编译成功。基类中 equals 方法的默认实现比较对象引用,如果两个引用变量都引用同一个对象,则返回 true,否则返回 false

因为 Book 类没有重写这个方法,所以 b1.equals(b2) 调用的是基类 Object 中的 equals 方法,它返回 false。因此,代码打印

false:false

[9.1] 使用 StringBuilder 类及其方法操作数据

ME-Q39)

以下哪些陈述是正确的?(选择 2 个选项。)

  1. StringBuilder sb1 = new StringBuilder() 将创建一个没有任何字符但具有存储 16 个字符的初始容量的 StringBuilder 对象。
  2. StringBuilder sb1 = new StringBuilder(5*10) 将创建一个具有值 50StringBuilder 对象。
  3. String 类不同,StringBuilder 中的 concat 方法会修改 StringBuilder 对象的值。
  4. insert 方法可以用来在 StringBuilder 的开始、结束或指定位置插入一个字符、数字或 String

答案:a, d

说明:StringBuilder 类中没有 concat 方法。它定义了一系列 append 方法(重载方法)来在 String-Builder 对象的末尾添加数据。

new StringBuilder(50) 创建一个没有任何字符但具有存储 50 个字符的初始容量的 StringBuilder 对象。

[4.1] 声明、实例化、初始化和使用一维数组

ME-Q40)

根据以下 Animal 类和 Jump 接口的定义,选择正确的数组声明和初始化(选择 3 个选项)。

interface Jump {}
class Animal implements Jump {}
  1. Jump eJump1[] = {null, new Animal()};
  2. Jump[] eJump2 = new Animal()[22];
  3. Jump[] eJump3 = new Jump[10];
  4. Jump[] eJump4 = new Animal[87];
  5. Jump[] eJump5 = new Jump()[12];

答案:a, c, d

说明:选项 (b) 是错误的,因为表达式的右侧试图通过括号 () 创建 Animal 类的单个对象。同时,它还在使用方括号 [] 来定义一个数组。这种组合是无效的。

选项 (e) 是错误的。除了使用无效的语法初始化数组(如前所述)外,它还试图创建 Jump 接口的对象。接口的对象不能被创建。

[9.4] 声明和使用指定类型的 ArrayList

[8.5] “识别常见的异常类(如 NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException、ClassCastException)”

ME-Q41)

以下代码的输出是什么?(选择 1 个选项。)

import java.util.*;
class EJGArrayL {
    public static void main(String args[]) {
        ArrayList<String> seasons = new ArrayList<>();
        seasons.add(1, "Spring"); seasons.add(2, "Summer");
        seasons.add(3, "Autumn"); seasons.add(4, "Winter");
        seasons.remove(2);
        for (String s : seasons)
            System.out.print(s + ", ");
    }
}
  1. 春季,夏季,冬季,
  2. 春季,秋季,冬季,
  3. 秋季,冬季,
  4. 编译错误
  5. 运行时异常

答案:e

说明:代码抛出一个运行时异常,IndexOutOfBoundsException,因为 ArrayList 正在尝试将其第一个元素插入位置 0。在第一次调用 add 方法之前,ArrayList seasons 的大小是 0。因为 season 的第一个元素存储在位置 0,所以将其第一个元素存储在位置 1 的调用将抛出 RuntimeException。如果较低位置可用,则 ArrayList 的元素不能添加到较高的位置。

[2.1] 声明和初始化变量(包括原始数据类型的转换)

[3.3] 创建 if 和 if/else 以及三元构造

[5.3] 创建和使用 do-while 循环

ME-Q42)

以下代码的输出是什么?(选择 1 个选项。)

class EIf {
    public static void main(String args[]) {
        bool boolean = false;
        do {
            if (boolean = true)
                System.out.println("true");
            else
                System.out.println("false");
        }
        while(3.3 + 4.7 > 8);    }
}
  1. 该类将打印 true
  2. 该类将打印 false
  3. 如果将 if 条件更改为 boolean == true,则该类将打印 true
  4. 如果将 if 条件更改为 boolean != true,则该类将打印 false
  5. 该类无法编译。
  6. 运行时异常

答案:e

说明:这个问题试图在两个点上欺骗你。首先,Java 中没有 bool 数据类型。其次,标识符的名称不能与保留字相同。代码试图使用名称 boolean 定义一个类型为 bool 的标识符。

[5.1] 创建和使用 while 循环

ME-Q43)

有多少条Fish被(如下定义的)Whale吃掉了?检查以下代码并选择正确的语句(选择 2 个选项)。

class Whale {
    public static void main(String args[]) {
        boolean hungry = false;

        while (hungry=true) {
            ++Fish.count;
        }
        System.out.println(Fish.count);
    }
}
class Fish {
    static byte count;
}
  1. 代码无法编译。
  2. 代码没有打印出任何值。
  3. 代码打印出0
  4. ++Fish.count改为Fish.count++将给出相同的结果。

答案:b, d

说明:选项(a)是错误的,因为代码编译成功。

选项(c)是错误的。这个问题试图通过在while结构中赋值时比较boolean值来欺骗你。因为while循环将值true赋给变量hungry,它将始终返回true,增加变量count的值,从而陷入无限循环。

选项(d)是正确的,因为当一元增量运算符(++)不是表达式的一部分时,后缀和前缀表示法的行为完全相同。

[5.2] 创建和使用包括增强型 for 循环在内的循环

ME-Q44)

给定以下代码,以下哪个选项,如果用来替换/* REPLACE CODE HERE */,将使代码打印出存储在数组phones中位置处的电话名称?(选择 1 个选项。)

class Phones {
    public static void main(String args[]) {
        String phones[]= {"BlackBerry", "Android", "iPhone"};
        for (String phone : phones)
            /* REPLACE CODE HERE */
    }
}
  1. System.out.println(phones.count + ":" + phone);
  2. System.out.println(phones.counter + ":" + phone);
  3. System.out.println(phones.getPosition() + ":" + phone);
  4. System.out.println(phones.getCtr() + ":" + phone);
  5. System.out.println(phones.getCount() + ":" + phone);
  6. System.out.println(phones.pos + ":" + phone);
  7. 以上都不是

答案:g

说明:增强型for循环不提供变量来访问它正在迭代的数组的当前位置。这种功能是常规for循环所提供的。

[2.5] 开发使用包装类(如 Boolean、Double 和 Integer)的代码。

[3.2] 使用==和 equals()测试字符串和其他对象之间的相等性

[7.3] 确定何时需要类型转换

[9.2] 创建和操作字符串

ME-Q45)

给定以下代码,

Byte b1 = (byte)100;                       // 1
Integer i1 = (int)200;                     // 2
Long l1 = (long)300;                       // 3
Float f1 = (float)b1 + (int)l1;            // 4
String s1 = 300;                           // 5
if (s1 == (b1 + i1))                       // 6
    s1 = (String)500;                      // 7
else                                       // 8
    f1 = (int)100;                         // 9
System.out.println(s1 + ":" + f1);         // 10

输出是什么?选择 1 个选项。

  1. 代码在第 1 行、第 3 行、第 4 行、第 7 行处编译失败。
  2. 代码在第 6 行、第 7 行处编译失败。
  3. 代码在第 7 行、第 9 行处编译失败。
  4. 代码在第 4 行、第 5 行、第 6 行、第 7 行、第 9 行处编译失败。
  5. 没有编译错误——输出500:300
  6. 没有编译错误——输出300:100
  7. 运行时异常

答案:d

说明:此题测试你对多个概念的理解:

  • 包装类的自动装箱和拆箱
  • 原始值和包装类之间的区别,例如,从longint以及将Long转换为int的类型转换
  • 原始值和对象的隐式和显式转换
  • 由于无效的显式转换而抛出的异常(隐式转换不会抛出任何异常)

第 1、2、3 行的代码不会抛出编译错误或运行时异常。你可以显式地将所有数值原始数据类型相互转换。例如,一个double原始值可以隐式转换为byte。一个char原始值可以显式转换为任何其他数值数据类型:

char c = 100;
Float f = (float)c;

第 4 行的代码将抛出编译错误。代码(int)l1不是将原始long类型转换为int类型。它试图将Long对象转换为原始int类型。你可以显式地将包装器对象转换为它所包装的类型。

第 5 行的代码无法编译,因为你不能使用赋值运算符将int值赋给String引用变量。你可以使用String方法valueOf()-,传递一个数值(整数或小数)以返回一个String实例。

第 6 行的代码无法编译,因为你不能将String实例与包装类实例进行比较。

第 7 行的代码无法编译。你不能显式地将数值字面量值转换为String值。

第 9 行的代码无法编译,因为int不能转换为Float

[3.2] 使用==equals()测试字符串和其他对象之间的相等性

[3.3] 创建 if 和 if/else 以及三元构造

[9.3] 使用 java.time.LocalDateTime、java.time.LocalDate、java.time.LocalTime、java.time.format.DateTimeFormatter、java.time.Period 类创建和操作日历数据

ME-Q46)

以下代码的输出是什么?(选择 1 个选项。)

class Book {
    String ISBN;
    Book(String val) {
        ISBN = val;
    }
    public boolean equals(Object b) {
        if (b instanceof Book) {
            return ((Book)b).ISBN.equals(ISBN);
        }

        else
            return false;
    }
}

class TestEquals {
    public static void main(String args[]) {
        Book b1 = new Book("1234-4657");
        Book b2 = new Book("1234-4657");
        LocalDate release = null;
        release = b1.equals(b2) ? b1 == b2? LocalDate.of(2050,12,12):
        LocalDate.parse("2072-02-01"):LocalDate.parse("9999-09-09");
        System.out.print(release);
    }
}
  1. 2050-12-12
  2. 2072-02-01
  3. 9999-09-09
  4. 编译错误
  5. 运行时异常

答案:b

解释:这个问题测试了多个概念:

  • 使用equals()和比较运算符==来确定对象相等性
  • 三元运算符的使用
  • 正确的方法和语法来实例化LocalDate

比较运算符确定引用变量是否引用同一对象。因为引用变量b1b2引用了不同的对象,所以b1==b2将返回false

方法equals是在类java.lang.Object中定义的public方法。因为Object类是 Java 中所有类的超类,所以equals被所有类继承。基类中equals的默认实现比较对象引用,如果两个引用变量都指向同一个对象则返回true,否则返回false。如果一个类重写了这个方法,它将返回一个boolean值,这个值取决于在这个类中定义的逻辑。Book类重写了equals方法,如果与被比较的Book对象定义相同的ISBN值,则返回true。因为两个变量b1b2ISBN对象值相同,所以b1.equals(b2)返回true

这里是三元运算符的语法:

variable = booleanValue? returnValueIfTrue : returnValueIfFalse;

三元运算符的所有组成部分都是必需的。与if结构不同,在三元运算符中你不能省略else部分。这个问题中的代码使用了嵌套的三元运算符。让我们缩进代码,这将使评估表达式变得容易:

release = b1.equals(b2) ?
            b1 == b2?
                LocalDate.of(2050,12,12):
                LocalDate.parse("2072-02-01"):
            LocalDate.parse("9999-09-09");

因为b1.equals(b2)返回true,控制流评估嵌套的三元运算符。因为b1 == b2返回false,代码返回三元运算符else部分中创建的LocalDate实例,即2072-02-01

[5.2] 创建和使用包括增强型 for 循环在内的循环

ME-Q47)

以下代码的输出是什么?(选择 1 个选项。)

int a = 10;
for (; a <= 20; ++a) {
    if (a%3 == 0) a++; else if (a%2 == 0) a=a*2;
    System.out.println(a);
}
  1. 11
    13
    15
    17
    19
    
  2. 20
    
  3. 11
    14
    17
    20
    
  4. 40
    
  5. 编译错误

答案:b

说明:这个问题需要多个技能:理解for循环的声明、使用运算符和使用if-else结构。

代码中的for循环定义正确。这个代码中的for循环没有使用其变量初始化块;它以;标记其变量初始化块不存在。if结构的代码故意不正确,因为你可能在考试中遇到类似的代码。

对于for循环的第一次迭代,变量a的值为10。因为a <= 20评估为true,控制流继续执行if结构。这个if结构可以正确缩进如下:

if (a%3 == 0)
    a++;
else if (a%2 == 0)
    a=a*2;

(a%3 == 0)的结果为false(a%2 == 0)的结果为true,因此将20 (a*2)的值赋给a。下一行打印a的值为20

循环语句的增量部分(++a),将变量a的值增加到21。对于下一次循环迭代,其条件评估为falsea <= 20),循环终止。

[6.1] 创建具有参数和返回值的函数;包括重载方法

ME-Q48)

给定以下代码,以下哪个选项如果用于替换//INSERT CODE HERE,将定义一个重载的rideWave方法?(选择 1 个选项。)

class Raft {
    public String rideWave() { return null; }
    //INSERT CODE HERE
}
  1. public String[] rideWave() { return null; }
  2. protected void riceWave(int a) {}
  3. private void rideWave(int value, String value2) {}
  4. default StringBuilder rideWave (StringBuffer a) { return null; }

答案:c

解释:选项(a)是错误的。仅更改方法返回值并不能定义一个有效的重载方法。

选项(b)是错误的。这个选项中方法的名称是riceWave而不是rideWave。重载方法应该有相同的名称。

选项(d)是错误的。default不是一个有效的访问修饰符。默认修饰符是通过缺少访问修饰符来标记的。

[5.2] 创建和使用包括增强型 for 循环的 for 循环

[5.5] 使用 break 和 continue

ME-Q49)

给定以下代码,哪个选项如果用于替换//INSERT CODE HERE,将正确计算数组num中所有偶数的总和并将其存储在变量sum中?(选择一个选项。)

int num[] = {10, 15, 2, 17};
int sum = 0;
for (int number : num) {
    //INSERT CODE HERE
    sum += number;
}
  1. if (number % 2 == 0)
        continue;
    
  2. if (number % 2 == 0)
        break;
    
  3. if (number % 2 != 0)
        continue;
    
  4. if (number % 2 != 0)
        break;
    

答案:c

解释:为了找到偶数的总和,首先需要确定一个数字是否是偶数。然后需要将偶数添加到变量sum中。

选项(c)确定数组元素是否完全可被2整除。如果不可以,它将使用continue语句跳过for循环中的剩余语句,从而从下一个数组元素开始执行for循环。如果数组元素可以完全被2整除,则continue不会执行,并将数组数字添加到变量sum中。

[3.1] 使用 Java 运算符;包括括号来覆盖运算符优先级

[9.5] 编写一个简单的 Lambda 表达式,该表达式消费一个 Lambda 谓词表达式

ME-Q50)

以下代码的输出是什么?(选择一个选项。)

import java.util.function.Predicate;
class Op {
    public static void main(String... args) {

        int a = 0;
        int b = 100;
        Predicate<Integer> compare = (var) -> var++ == 10;
        if (!b++ > 100 && compare.test(a)) {
            System.out.println(a+b);
        }
    }
}
  1. 100
  2. 101
  3. 102
  4. 代码无法编译。
  5. 没有输出。

答案:d

解释:代码正确地定义了 Lambda 表达式。

虽然看起来一元否定运算符!被应用于表达式b++ > 100,但实际上它是被应用于类型为int的变量b。因为一元否定运算符!不能应用于类型为int的变量,所以代码无法编译。正确的if条件如下:

if (!(b++ > 100) && compare.test(a)) {

[6.5] 将封装原则应用于类

ME-Q51)

选择符合以下规范的选项:创建一个良好封装的类Pencil,其中有一个实例变量modelmodel的值应该在Pencil外部可访问和可修改。(选择一个选项。)

  1. class Pencil {
        public String model;
    }
    
  2. class Pencil {
        public String model;
        public String getModel() { return model; }
        public void setModel(String val) { model = val; }
    }
    
  3. class Pencil {
        private String model;
        public String getModel() { return model; }
        public void setModel(String val) { model = val; }
    }
    
  4. class Pencil {
        public String model;
        private String getModel() { return model; }
        private void setModel(String val) { model = val; }
    }
    

答案:c

说明:一个良好封装的类的实例变量不应直接在类外访问。它们应通过非 private 的 getter 和 setter 方法访问。

[7.2] 编写代码以演示多态的使用;包括重写和对象类型与引用类型

ME-Q52)

以下代码的输出是什么?(选择一个选项。)

class Phone {
    void call() {
        System.out.println("Call-Phone");
    }
}
class SmartPhone extends Phone{
    void call() {
        System.out.println("Call-SmartPhone");
    }
}
class TestPhones {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone smartPhone = new SmartPhone();
        phone.call();
        smartPhone.call();
    }
}
  1. Call-Phone
    Call-Phone
    
  2. Call-Phone
    Call-SmartPhone
    
  3. Call-Phone
    null
    
  4. null
    Call-SmartPhone
    

答案:b

说明:方法 call 在基类 Phone 中定义。此 call 方法被派生类 SmartPhone 继承并重写。两个引用变量 phonesmartphone 的类型都是 Phone。但是,引用变量 phone 指向 Phone 类的对象,而变量 smartPhone 指向 SmartPhone 类的对象。当在引用变量 smartPhone 上调用 call 方法时,它调用的是 SmartPhone 类中定义的 call 方法,因为对重写方法的调用是在运行时解析的,并且基于调用方法的对象的类型。

[7.2] 编写代码以演示多态的使用;包括重写和对象类型与引用类型

ME-Q53)

以下代码的输出是什么?(选择一个选项。)

class Phone {
    String keyboard = "in-built";
}
class Tablet extends Phone {
    boolean playMovie = false;
}
class College2 {
    public static void main(String args[]) {
        Phone phone = new Tablet();
        System.out.println(phone.keyboard + ":" + phone.playMovie);
    }
}
  1. in-built:false
  2. in-built:true
  3. null:false
  4. null:true
  5. 编译错误

答案:e

说明:此代码无法编译。类型为 Phone 的对象引用变量 phone 可以用来引用其派生类型 Tablet 的对象。但是,基类变量不能访问其派生类的变量和方法,除非显式转换为派生类的对象。因此,phone 可以访问 keyboard,但不能访问 playMovie

[2.1] 声明和初始化变量(包括原始数据类型的转换)

ME-Q54)

以下代码的输出是什么?(选择一个选项。)

public class Wall {
    public static void main(String args[]) {
        double area = 10.98;
        String color;
        if (area < 5)
            color = "red";
        else
            color = "blue";
        System.out.println(color);
    }
}
  1. red
  2. blue
  3. 无输出
  4. 编译错误

答案:b

说明:当你回答一个包含访问局部变量值的问题时,检查它是否已初始化。尝试访问未初始化的局部变量的代码将无法编译。

在这个问题中,局部变量 color 使用 if-else 结构进行初始化。如果另一个局部变量(area)的值小于 5,则 color 将初始化为 "red",否则将初始化为 "blue"。因此,无论 area 的值如何,color 都一定会被初始化。在 if-else 结构中,至少会执行两个代码块中的一个,对应于 ifelse。代码将成功执行,打印 blue

注意使用条件语句初始化局部变量的代码。如果编译器似乎无法保证局部变量的初始化,尝试访问它的代码将无法编译。以下是本问题中包含的代码的修改版本(加粗部分为修改),该代码无法编译:

public class Wall {
    public static void main(String args[]) {
        double area = 10.98;
        String color;
        if (area < 5)
            color = "red";
        if (area >= 5)
            color = "blue";
        System.out.println(color);
    }
}

[2.3] 了解如何读取或写入对象字段

ME-Q55)

以下代码的输出是什么?(选择 1 个选项。)

class Diary {
    int pageCount = 100;
    int getPageCount() {
        return pageCount;
    }
    void setPageCount(int val) {
        pageCount = val;
    }
}

class ClassRoom {
    public static void main(String args[]) {
        System.out.println(new Diary().getPageCount());
        new Diary().setPageCount(200);
        System.out.println(new Diary().getPageCount());
    }
}
  1. 100
    200
    
  2. common7.jpg

  3. 100
    100
    
  4. 200
    200
    
  5. 代码无法编译。

答案:b

说明:类的构造函数创建并返回定义该类的对象。返回的对象可以被分配给引用变量。如果返回的对象没有被分配给任何引用变量,则无法再次访问该对象的任何变量或方法。这就是在ClassRoom类中发生的情况。示例中对getPageCountsetPageCount方法的调用操作的是无关对象。

[5.3] 创建和使用 do-while 循环

ME-Q56)

你认为你可以用以下代码(即以下代码的输出是什么)购物多少次?(选择 1 个选项。)

class Shopping {
    public static void main(String args[]) {
        boolean bankrupt = true;
        do System.out.println("enjoying shopping"); bankrupt = false;
        while (!bankrupt);
    }
}
  1. 代码只打印了一次enjoying shopping
  2. 代码打印了两次enjoying shopping
  3. 代码在无限循环中打印enjoying shopping
  4. common7.jpg 代码无法编译。

答案:d

说明:代码无法编译,因为它试图在dowhile语句之间放置两行代码,而没有使用花括号。

[4.2] 声明、实例化、初始化和使用多维数组

ME-Q57)

以下哪些选项是定义多维数组的有效选项?(选择 4 个选项。)

  1. common6.jpg String ejg1[][] = new String[1][2];
  2. common6.jpg String ejg2[][] = new String[][] { {}, {} };
  3. common6.jpg String ejg3[][] = new String[2][2];
  4. common6.jpg String ejg4[][] = new String[][]{{null},new String[]{"a","b","c"}, {new String()}};
  5. String ejg5[][] = new String[][2];
  6. String ejg6[][] = new String[][]{"A", "B"};
  7. String ejg7[][] = new String[]{{"A"}, {"B"}};

答案:a, b, c, d

说明:选项(a)、(b)、(c)和(d)正确地定义了多维数组。

选项(e)是错误的,因为第一个方括号中的大小缺失。

选项(f)是错误的。正确的代码必须在右侧使用额外的对 {},如下所示:

String ejg6[][] = new String[][]{{"A"}, {"B"}};

选项(g)是错误的。正确的代码必须在右侧使用额外的 [],如下所示:

String ejg7[][] = new String[][]{{"A"}, {"B"}};

[6.6] 确定当它们传递到修改值的函数时,对象引用和原始值的影响

ME-Q58)

以下代码的输出是什么?(选择 1 个选项。)

class Laptop {
    String memory = "1GB";
}
class Workshop {
    public static void main(String args[]) {
        Laptop life = new Laptop();
        repair(life);
        System.out.println(life.memory);
    }
    public static void repair(Laptop laptop) {
        laptop = new Laptop();
        laptop.memory = "2GB";
    }
}
  1. 1 GB
  2. 2 GB
  3. 编译错误
  4. 运行时异常

答案:a

解释:在这个例子中定义的 repair 方法将一个新的对象分配给传递给它的方法参数 laptop。然后它通过将 1 GB 分配给其实例变量 memory 来修改这个新分配对象的属性。

当一个方法重新分配传递给它的对象引用变量时,对其状态的修改在调用方法中是不可见的。这是因为修改是针对一个新的对象进行的,而不是最初传递给这个方法的那个对象。repair 方法将一个新的对象分配给传递给它的引用变量 laptop 并修改其状态。因此,对方法参数 laptop 状态的修改在 main 方法中是不可见的,并且它打印 life.memory 的值为 1 GB

[7.3] 确定何时需要转换

ME-Q59)

给定以下代码,以下哪个选项(如果用于替换 //INSERT CODE HERE),将使类型为 Roamable 的引用变量能够引用 Phone 类的对象?(选择 1 个选项。)

interface Roamable{}
class Phone {}
class Tablet extends Phone implements Roamable {
    //INSERT CODE HERE
}
  1. Roamable var = new Phone();
  2. Roamable var = (Roamable)Phone();
  3. Roamable var = (Roamable)new Phone();
  4. 因为接口 Roamable 和类 Phone 是无关的,所以类型为 Roamable 的引用变量不能引用 Phone 类的对象。

答案:c

解释:选项(a)是错误的。没有显式转换,类型为 Roamable 的引用变量不能引用 Phone 类的对象。

选项(b)是错误的,因为这行代码是无效的,将无法编译。

选项(d)是错误的,因为类型为 Roamable 的引用变量可以通过显式转换引用 Phone 类的对象。

注意,尽管选项(c)可以编译,但如果执行它将抛出 ClassCastException

[7.4] 使用 super 和 this 访问对象和构造函数

ME-Q60)

以下代码的输出是什么?(选择 1 个选项。)

class Paper {
    Paper() {
        this(10);
        System.out.println("Paper:0");
    }
    Paper(int a) { System.out.println("Paper:1"); }
}
class PostIt extends Paper {}
class TestPostIt {
    public static void main(String[] args) {
        Paper paper = new PostIt();
    }
}
  1. Paper:1
    
  2. Paper:0
    
  3. Paper:0
    Paper:1
    
  4. Paper:1
    Paper:0
    

答案:d

解释:new PostIt() 通过调用其编译器提供的无参数构造函数创建 PostIt 类的对象。PostIt 类的无参数构造函数调用其基类的无参数构造函数,该基类的无参数构造函数调用接受一个 int 方法参数的其他构造函数。接受 int 参数的构造函数打印 Paper:1 然后返回控制权到无参数构造函数。无参数构造函数然后打印 Paper:0

[9.1] 使用 StringBuilder 类及其方法操作数据

ME-Q61)

检查以下代码并选择正确的陈述(选择 1 个选项)。

line1> class StringBuilders {
line2>     public static void main(String... args) {
line3>         StringBuilder sb1 = new StringBuilder("eLion");
line4>         String ejg = null;
line5>         ejg = sb1.append("X").substring(sb1.indexOf("L"), sb1.indexOf("X"));
line6>         System.out.println(ejg);
line7>     }
line8> }
  1. 代码将打印 LionX

  2. 代码将打印 Lion

  3. 如果将第 5 行代码更改为以下内容,代码将打印 Lion

  4. ejg = sb1.append("X").substring(sb1.indexOf('L'), sb1.indexOf('X'));
    
  5. 只有当第 4 行代码更改为以下内容时,代码才能编译:

  6. StringBuilder ejg = null;
    

答案:b

解释:选项 (a) 是错误的,选项 (b) 是正确的。substring 方法不包含它返回的结果中的结束位置的字符。因此,代码打印 Lion

选项 (c) 是错误的。如果按此选项更改第 5 行代码,代码将无法编译。你不能将 char 传递给 StringBuilderindexOf 方法;它接受 String

选项 (d) 是错误的,因为代码没有编译问题。

[7.5] 使用抽象类和接口

ME-Q62)

给定以下代码,

interface Jumpable {
    int height = 1;
    default void worldRecord() {
        System.out.print(height);
    }
}
interface Moveable {
    int height = 2;
    static void worldRecord() {
        System.out.print(height);
    }

}
class Chair implements Jumpable, Moveable {
    int height = 3;
    Chair() {
        worldRecord();
    }
    public static void main(String args[]) {
        Jumpable j = new Chair();
        Moveable m = new Chair();
        Chair c = new Chair();
    }
}

输出是什么?选择 1 个选项。

  1. 111
  2. 123
  3. 333
  4. 222
  5. 编译错误
  6. 运行时异常

答案:a

解释:类 Chair 的构造函数调用接口 Jumpable 中定义的默认非 static 方法。此外,如果只定义了接口 Moveable 中的 static world-Record() 方法,其调用必须限定(即,Moveable.worldRecord();),以便类 Chair 能够编译。

[7.3] 确定何时需要类型转换

ME-Q63)

给定以下代码,以下哪个选项(如果用于替换 /* INSERT CODE HERE */),将使类 Jungle 能够确定引用变量 animal 是否指向类 Lion 的对象并打印 1?(选择 1 个选项。)

class Animal{ float age; }
class Lion extends Animal { int claws;}
class Jungle {
    public static void main(String args[]) {
        Animal animal = new Lion();
        /* INSERT CODE HERE */
            System.out.println(1);
    }
}
  1. if (animal instanceof Lion)
  2. if (animal instanceOf Lion)
  3. if (animal == Lion)
  4. if (animal = Lion)

答案:a

解释:选项 (b) 是错误的,因为正确的操作符名称是 instanceof 而不是 instanceOf(注意大写的 O)。

选项 (c) 和 (d) 是错误的。这两行代码都无法编译,因为它们试图将类名比较和赋值给变量,这是不允许的。

[6.3] 创建和重载构造函数;包括对默认构造函数的影响

ME-Q64)

假设文件 Test.java,其中定义了以下代码,无法编译,选择编译失败的原因(选择 2 个选项)。

class Person {
    Person(String value) {}
}
class Employee extends Person {}
class Test {
    public static void main(String args[]) {
        Employee e = new Employee();
    }
}
  1. Person 编译失败。
  2. Employee 编译失败。
  3. 默认构造函数只能调用基类的无参数构造函数。
  4. 在类 Test 中创建 Employee 类对象的代码没有将 String 值传递给 Employee 类的构造函数。

答案:b, c

解释:类 Employee 无法编译,因此类 Test 无法使用类型为 Employee 的变量,并且编译失败。

在尝试编译类 Employee 时,Java 编译器为它生成一个默认构造函数,其外观如下:

Employee() {
    super();
}

注意,派生类的构造函数必须始终调用基类的构造函数。当 Java 为类 Employee 生成之前的默认构造函数时,它无法编译,因为基类没有无参构造函数。Java 生成的默认构造函数只能定义对基类无参构造函数的调用。它不能调用其他基类构造函数。

[6.3] 创建并重载构造函数;包括对默认构造函数的影响

ME-Q65

检查以下代码并选择正确的语句(选择 2 个选项)。

class Bottle {
    void Bottle() {}
    void Bottle(WaterBottle w) {}
}
class WaterBottle extends Bottle {}
  1. 基类不能在其构造函数中将定义的类的引用变量作为方法参数传递。
  2. 类编译成功——基类可以使用其派生类的引用变量作为方法参数。
  3. Bottle 定义了两个重载的构造函数。
  4. Bottle 只能访问一个构造函数。

答案:b, d

说明:基类可以使用其派生类的引用变量和对象。请注意,类 Bottle 中定义的方法不是构造函数,而是具有名称 Bottle 的常规方法。构造函数的返回类型不是 void

[7.4] 使用 super 和 this 访问对象和构造函数

ME-Q66

给定以下代码,以下哪个选项,如果用于替换 /* INSERT CODE HERE */,将导致代码打印 110?(选择 1 个选项。)

class Book {
    private int pages = 100;
}
class Magazine extends Book {
    private int interviews = 2;
    private int totalPages() { /* INSERT CODE HERE */ }

    public static void main(String[] args) {
        System.out.println(new Magazine().totalPages());
    }

}
  1. return super.pages + this.interviews*5;
  2. return this.pages + this.interviews*5;
  3. return super.pages + interviews*5;
  4. return pages + this.interviews*5;
  5. 以上皆非

答案:e

说明:变量 pages 在类 Book 中具有 private 访问权限,并且不能从类外访问。

[8.4] 创建并调用抛出异常的方法

[8.5] “识别常见的异常类(如 NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException, ClassCastException)”

ME-Q67

给定以下代码,

class NoInkException extends Exception {}
class Pen{
    void write(String val) throws NoInkException {
        int c = (10 - 7)/ (8 - 2 - 6);
    }
    void article() {
        //INSERT CODE HERE
    }
}

//INSERT CODE HERE 插入以下哪个选项,将定义在方法 article 中对方法 write 的有效使用?(选择 2 个选项。)

  1. try {
        new Pen().write("story");
    } catch (NoInkException e) {}
    
  2. try {
        new Pen().write("story");
    } finally {}
    
  3. try {
        write("story");
    } catch (Exception e) {}
    
  4. try {
        new Pen().write("story");
    } catch (RuntimeException e) {}
    

答案:a, c

说明:在执行过程中,方法 write 将由于除以 0 而始终抛出 Arithmetic-Exception(一个 RuntimeException)。但是,此方法声明抛出 NoInkException,这是一个检查型异常。

因为 NoInkException 继承自类 Exception 而不是 RuntimeException,所以 NoInkException 是一个检查型异常。当你调用抛出检查型异常的方法时,你可以通过 try-catch 块来处理它,或者在方法签名中声明它将被抛出。

选项 (a) 是正确的,因为对 write 方法的调用被包含在一个 try 块中。try 块后面跟着一个 catch 块,它定义了对 NoInkException 异常的处理程序。

选项 (b) 是不正确的。对 write 方法的调用被包含在一个 try 块中,后面跟着一个 finally 块。finally 块不用于处理异常。

选项 (c) 是正确的。因为 NoInkExceptionException 的子类,所以 Exception 类的异常处理程序也可以处理 NoInkException 异常。

选项 (d) 是不正确的。此选项为 RuntimeException 类定义了一个异常处理程序。因为 NoInkException 不是 RuntimeException 的子类,所以此代码不会处理 NoInkException

[1.1] 定义变量的作用域

ME-Q68)

以下代码的输出是什么?(选择一个选项。)

class EMyMethods {
    static String name = "m1";
    void riverRafting() {
        String name = "m2";
        if (8 > 2) {
            String name = "m3";
            System.out.println(name);
        }
    }
    public static void main(String[] args) {
        EMyMethods m1 = new EMyMethods();
        m1.riverRafting();
    }
}
  1. m1
  2. m2
  3. m3
  4. 代码无法编译。

答案:d

说明:类 EMyMethods 定义了三个名为 name 的变量:

  • 带有值 "m1"static 变量 name
  • 方法 riverRafting 中的局部变量 name,其值为 "m2"
  • 变量 name,在方法 riverRaftingif 块中局部定义,其值为 "m3"

代码由于在方法 riverRafting 中定义了两个具有相同名称(name)的局部变量而无法编译。如果允许此代码编译,这些局部变量的作用域将重叠——在 if 块外部定义的变量 name 将可以访问整个方法 riverRafting。在 if 块内定义的局部变量 name 的作用域将限制在 if 块内。

if 块内,你认为代码会如何区分这些局部变量?因为没有方法可以做到这一点,所以代码无法编译。

[9.2] 创建和操作字符串

ME-Q69)

以下代码的输出是什么?(选择一个选项。)

class EBowl {
    public static void main(String args[]) {
        String eFood = "Corn";
        System.out.println(eFood);
        mix(eFood);
        System.out.println(eFood);
    }
    static void mix(String foodIn) {
        foodIn.concat("A");
        foodIn.replace('C', 'B');
    }
}
  1. Corn
    BornA
    
  2. Corn
    CornA
    
  3. Corn
    Born
    
  4. Corn
    Corn
    

答案:d

说明:String 对象是不可变的。这意味着使用任何方法都无法更改 String 变量的值。在这种情况下,String 对象被传递给一个方法,看起来似乎可以,但实际上并没有改变 String 的内容。

[3.4] 使用 switch 语句

ME-Q70)

以下代码的哪个陈述是正确的?(选择一个选项。)

class SwJava {
    public static void main(String args[]) {
        String[] shapes = {"Circle", "Square", "Triangle"};

        switch (shapes) {
            case "Square": System.out.println("Circle"); break;
            case "Triangle": System.out.println("Square"); break;
            case "Circle": System.out.println("Triangle"); break;
        }
    }
}
  1. 代码打印 Circle

  2. 代码打印 Square

  3. 代码打印 Triangle

  4. 代码打印

  5. Circle
    Square
    Triangle
    
  6. 代码打印

  7. Triangle
    Circle
    Square
    
  8. 代码无法编译。

答案:f

解释:这个问题试图欺骗你;它通过传递一个 String[] 值给 switch 构造,传递一个 String 对象数组。代码无法编译,因为数组不是 switch 构造的有效参数。如果传递数组 shapes 的一个元素(shapes[0]shapes[1]shapes[2]),代码将可以编译。

[8.4] 创建并调用抛出异常的方法

ME-Q71)

给定 PersonFatherHome 类的定义,以下哪些选项(选择 3 个选项)如果用于替换 //INSERT CODE HERE,将导致代码成功编译?

class Person {}
class Father extends Person {
    public void dance() throws ClassCastException {}
}
class Home {
    public static void main(String args[]) {
        Person p = new Person();
        try {
            ((Father)p).dance();
        }
        //INSERT CODE HERE
    }
}
  1. catch (NullPointerException e) {}
    catch (ClassCastException e) {}
    catch (Exception e) {}
    catch (Throwable t) {}
    
  2. catch (ClassCastException e) {}
    catch (NullPointerException e) {}
    catch (Exception e) {}
    catch (Throwable t) {}
    
  3. catch (ClassCastException e) {}
    catch (Exception e) {}
    catch (NullPointerException e) {}
    catch (Throwable t) {}
    
  4. catch (Throwable t) {}
    catch (Exception e) {}
    catch (ClassCastException e) {}
    catch (NullPointerException e) {}
    
  5. finally {}
    

答案:a, b, e

解释:因为 NullPointerExceptionClassCastException 不共享基类-派生类关系,所以它们可以相互之前或之后放置。

Throwable 类是 Exception 的基类。因此,Throwable 类的异常处理器不能放在 Exception 类的异常处理器之前。同样,ExceptionNullPointerExceptionClassCastException 的基类。因此,Exception 类的异常处理器不能放在 ClassCastExceptionNullPointer-Exception 类的异常处理器之前。

选项 (e) 是可以的,因为没有定义要抛出的检查异常。

[2.1] 声明和初始化变量(包括原始数据类型的转换)

[5.1] 创建和使用 while 循环

[9.3] 使用 java.time.LocalDateTime、java.time.LocalDate、java.time.LocalTime、java.time.format.DateTimeFormatter、java.time.Period 类创建和操作日历数据

ME-Q72)

以下代码的输出是什么?(选择 1 个选项。)

import java.time.*;
class Camera {
    public static void main(String args[]) {
        int hours;

        LocalDateTime now = LocalDateTime.of(2020, 10, 01, 0 , 0);
        LocalDate before = now.toLocalDate().minusDays(1);
        LocalTime after = now.toLocalTime().plusHours(1);

        while (before.isBefore(after) && hours < 4) {
            ++hours;
        }
        System.out.println("Hours:" + hours);
    }
}
  1. 代码打印 Camera:null
  2. 代码打印 Camera:Adjust settings manually.
  3. 代码打印 Camera:.
  4. 代码无法编译。

答案:d

解释:注意变量 nowbeforeafter 的类型——它们并不相同。代码无法编译是因为代码 before.isBefore(after)LocalDate 实例上调用 isBefore 方法,并传递一个 LocalTime 实例,这是不允许的。

局部变量 hours 在被引用于 while 条件 (hours < 4) 之前没有被初始化,这是类无法编译的另一个原因。

[6.2] 将静态关键字应用于方法和字段

ME-Q73)

定义如下类的 TestEJavaCourse 的输出是 300

class Course {
    int enrollments;
}
class TestEJavaCourse {
    public static void main(String args[]) {
        Course c1 = new Course();
        Course c2 = new Course();
        c1.enrollments = 100;
        c2.enrollments = 200;
        System.out.println(c1.enrollments + c2.enrollments);
    }
}

如果将变量 enrollments 定义为 static 变量会发生什么?(选择 1 个选项。)

  1. 输出无变化。TestEJavaCourse 打印 300
  2. 输出变化。TestEJavaCourse 打印 200
  3. 图片 输出变化。TestEJavaCourse 打印 400
  4. TestEJavaCourse 编译失败。

答案:c

解释:将变量 enrollments 的定义更改为 static 变量后,代码不会在编译时失败。static 变量可以使用定义它的类的变量引用来访问。一个类的所有对象共享同一个 static 变量的副本。当使用引用变量 c2 修改变量 enrollments 时,c1.enrollments 也等于 200。因此,代码打印出 200 + 200 的结果,即 400

[4.2] 声明、实例化、初始化和使用多维数组

ME-Q74)

以下代码的输出是什么?(选择一个选项。)

String ejgStr[] = new String[][]{{null},new String[]{"a","b","c"},{new String()}}[0] ;
String ejgStr1[] = null;
String ejgStr2[] = {null};

System.out.println(ejgStr[0]);
System.out.println(ejgStr2[0]);
System.out.println(ejgStr1[0]);
  1. null
    NullPointerException
    
  2. 图片

  3. null
    null
    NullPointerException
    
  4. NullPointerException
    
  5. null
    null
    null
    Answer: b
    

解释:在这段代码中,最棘手的赋值是变量 ejgStr 的赋值。以下代码行可能 看起来(但实际上并不)将一个二维 String 数组赋值给变量 ejgStr

String ejgStr[] = new String[][]{{null},new String[]{"a","b","c"},{new String()}}[0] ;

前面的代码将二维 String 数组的第一个元素赋值给变量 ejgStr。以下缩进的代码将使前面的语句更容易理解:

图片

因此,让我们看看简化的赋值:

String ejgStr[] = {null};
String ejgStr1[] = null;
String ejgStr2[] = {null};

回顾打印数组元素的代码:

图片

因为 ejgStr 指向长度为 1 的数组 ({null}),所以 ejgStr[0] 打印 nullejgStr2 也指向长度为 1 的数组 ({null}),所以 ejgStr2[0] 也打印 nullejgStr1 指向 null,而不是数组。尝试访问 ejgStr1 的第一个元素会抛出 NullPointerException

[9.3] 使用 java.time.LocalDateTime、java.time.LocalDate、java.time.LocalTime、java.time.format.DateTimeFormatter、java.time.Period 类创建和操作日历数据

[9.4] 声明并使用指定类型的 ArrayList

ME-Q75)

检查以下代码并选择正确的陈述(选择一个选项)。

import java.util.*;
class Person {}
class Emp extends Person {}

class TestArrayList {
    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<>();
        list.add(new String("1234"));                 //LINE1
        list.add(new Person());                       //LINE2
        list.add(new Emp());                          //LINE3
        list.add(new String[]{"abcd", "xyz"});        //LINE4
        list.add(LocalDate.now().plus(1));            //LINE5
    }
}
  1. 第 1 行的代码无法编译。
  2. 第 2 行的代码无法编译。
  3. 第 3 行的代码无法编译。
  4. 第 4 行的代码无法编译。
  5. 图片 第 5 行的代码无法编译。
  6. 以上皆非。
  7. 从 (a) 到 (e) 的所有选项。

答案:e

解释:ArrayList 的类型决定了可以添加到其中的对象类型。ArrayList 可以添加其派生类的所有对象。选项 (a) 到 (d) 可以编译,因为 Object 类是所有 Java 类的超类;在本题中定义的 ArrayList list 将接受所有类型的对象,包括数组,因为它们也是对象。

尽管可以将LocalDate实例添加到ArrayList中,但选项(e)中的代码将无法编译。LocalDate.now()返回一个LocalDate实例。但LocalDate类没有定义接受要添加到其中的整数值的plus()方法——实际上有一个接受TemporalAmount实例的plus方法。你可以使用以下任何方法将天数、月份、周或年添加到LocalDate,传递long值:

  • plusDays(long daysToAdd)
  • plusMonths(long monthsToAdd)
  • plusWeeks(long weeksToAdd)
  • plusYears(long yearsToAdd)

你也可以使用以下方法将天数添加到LocalDate,传递一个Period实例(Period实现了TemporalAmount):

plus(TemporalAmount amountToAdd)

[3.3] 创建 if 和 if/else 以及三元构造

ME-Q76)

以下代码的输出是什么?(选择 1 个选项。)

public class If2 {
    public static void main(String args[]) {
        int a = 10; int b = 20; boolean c = false;
        if (b > a) if (++a == 10) if (c!=true) System.out.println(1);
        else System.out.println(2); else System.out.println(3);
    }
}
  1. 1
  2. 2
  3. 图片 3
  4. 没有输出

答案:c

解释:回答关于未缩进的代码问题的关键是缩进它。下面是如何做的:

if (b > a)
    if (++a == 10)
        if (c!=true)
            System.out.println(1);
        else
            System.out.println(2);
    else System.out.println(3);

现在代码看起来更简单,更容易理解。记住,最后的else语句属于内部if (++a == 10)。正如你所见,if (++a == 10)评估为false,代码将打印3

[7.5] 使用抽象类和接口

ME-Q77)

给定以下代码,

interface Movable {
    default int distance() {
        return 10;
    }
}
interface Jumpable {
    default int distance() {
        return 10;
    }
}

哪些选项正确地定义了实现接口MovableJumpable的类Person?(选择 1 个选项。)

  1. class Person implements Movable, Jumpable {}
    
  2. class Person implements Movable, Jumpable {
        default int distance() {
            return 10;
        }
    }
    
  3. 图片

  4. class Person implements Movable, Jumpable {
        public int distance() {
            return 10;
        }
    }
    
  5. class Person implements Movable, Jumpable {
        public long distance() {
            return 10;
        }
    }
    
  6. class Person implements Movable, Jumpable {
        int distance() {
            return 10;
        }
    }
    

答案:c

解释:选项(a)是错误的,因为类Person不能实现同时定义了具有相同签名和默认实现的distance方法的JumpableMovable这两个接口。类Person必须覆盖distance()的默认实现以实现这两个接口。

选项(b)是错误的。当一个类覆盖从接口继承的方法的默认实现时,它不能使用关键字default。这样的代码将无法编译。

选项(d)是错误的。返回类型为longdistance()方法不能覆盖返回类型为intdistance()方法。

选项(e)是错误的,并且这段代码将无法编译。类Person试图将distance()的访问级别从public降低到default

附录. 故事中的转折练习答案

第一章至第七章包含多个故事中的转折练习。这些练习的答案以及详细的解释见本附录。每个练习的答案包括以下元素:

  • 目的— 练习的目标(每个练习试图吸引你注意的转折)

  • 答案— 正确答案

  • 解释— 对答案的全面解释

让我们从第一章开始吧。

A.1 第一章:Java 基础知识

第一章包含四个故事中的转折练习。

A.1.1 故事中的转折 1.1

目的:这个练习鼓励你使用 Java 源代码文件的正确内容(类和接口)的组合来练习代码。

答案:c, d

解释:选项(a)和(b)是错误的。

选项(c)是正确的,因为 Java 源代码文件可以定义多个接口和类。

选项(d)是正确的,因为在具有匹配名称的 Java 源代码文件中可以定义public接口或类。public接口Printable不能在 Java 源代码文件 Multiple.java 中定义。它必须在 Printable.java 中定义。

A.1.2 故事中的转折 1.2

目的:尽管与故事中的转折 1.1 相似,但这个问题在措辞和意图方面有所不同。它要求你选择单独正确的选项。选择一个单独正确的选项意味着该选项应该单独正确,而不是与其他任何选项结合。你可能在真正的考试中遇到类似的问题。

答案:a, c, d

解释:选项(a)是正确的,而(b)是错误的,因为 Multiple2.java 无法编译。Multiple2.java 不能定义一个publicCar

选项(c)是正确的,因为从 Multiple2.java 中删除publicCar的定义将在 Multiple2.java 中留下一个唯一的public接口——Multiple2。因为public接口Multiple2和源代码文件的名称匹配,所以 Multiple2.java 将成功编译。

选项(d)是正确的。将publicCar更改为非public类将在 Multiple2.java 中留下一个唯一的public接口——Multiple2。因为public接口Multiple2和源代码文件的名称匹配,所以 Multiple2.java 将成功编译。

选项(e)是错误的。如果你将public接口Multiple2的访问修饰符更改为非public,Multiple2.java 将包含一个publicCar的定义,这是不允许的。

A.1.3 故事中的转折 1.3

目的:这个练习鼓励你执行选项中的代码,以理解方法main的正确方法签名以及传递给它的方法参数。

答案:a, b

说明:这个问题中的所有选项都应该使用命令 javaEJava java one one 来执行。每个术语的目的如下:

  • 术语 1,java—用于执行 Java 类

  • 术语 2,EJava—要执行的类名

  • 术语 3,java—作为 main 方法的第一个参数传递

  • 术语 4,one—作为 main 方法的第二个参数传递

  • 术语 5,one—作为 main 方法的第三个参数传递

要输出 java onemain 方法应该输出传递给它的第一个参数以及第二个或第三个参数。

选项 (a) 和 (b) 是正确的,因为它们使用了方法 main 的正确方法签名。方法参数的名称不必是 args。它可以是一个其他有效的标识符。选项 (a) 输出传递给它的第一个和第三个参数的值。选项 (b) 输出传递给它的第一个和第二个参数的值。

选项 (c) 是错误的,因为这个 main 方法接受一个二维数组。因此,它不会被当作 the main 方法处理。

选项 (d) 是错误的,因为这个代码无法编译。方法的访问修饰符(public)应该放在其返回类型(void)之前;否则,代码无法编译。

A.1.4 “故事中的转折”1.4

目的:除了确定可以限制类在包内可见性的正确访问修饰符之外,这个练习还希望你尝试不同的访问修饰符,这些修饰符可以用来声明一个类。

答案:哈里提交的代码。

说明:保罗提交的代码是不正确的,因为当使用 public 访问修饰符定义类 Curtain 时,它将在包 building 外部可访问。

淑瑞亚和塞尔万提交的代码是不正确的,因为类 Curtain 是一个顶级类(它不是在另一个类中定义的),因此不能使用 protectedprivate 访问修饰符来定义。

A.2 第二章:使用 Java 数据类型

第二章 包含四个“故事中的转折”练习。故事中的转折 2.1 有两部分。

A.2.1 “故事中的转折”2.1(第一部分)

目的:默认情况下,System.out.println() 会以其十进制基数打印数字。它这样做,无论你使用什么基数数系统来初始化数字。

答案:该代码打印以下输出:

534
534

说明:程序员经常被类似的问题所迷惑。如果一个变量使用 0b100001011(二进制数系统中的数字)赋值,程序员可能会认为 System.out.println() 会以二进制数系统打印数字,这是不正确的。默认情况下,System.out.println() 会以十进制基数打印数字。所有四个变量 baseDecimaloctValhexValbinVal 分别代表十进制、八进制、十六进制和二进制数系统中的十进制值 267。加法运算将这些值相加并打印 534 两次。

你可以使用Integer类中的方法以如下方式打印出二进制数系统中的值:

System.out.println(Integer.toBinaryString(0b100001011));

注意,Integer类不在此考试中,你不会就它提出任何问题。这个类仅用于参考。

A.2.2 故事中的转折 2.1(第二部分)

目的:Java 7 语言的一个新特性是在字面数值中使用下划线。本练习的目的是帮助你熟悉这个特性,如果你之前没有在字面数值中使用下划线的话。

答案:只有var1var6var7正确地定义了字面整数值。

说明:由var2定义的字面值0_x_4_13是不正确的,因为它在起始的0和字母x之后使用了下划线,这两者都是不允许的。正确的值是0x4_13

var3定义的字面值0b_x10_BA_75是不正确的。你不能在用于定义二进制字面值的0b0B前缀之后放置下划线。此外,二进制值只能包含数字10

由值var4定义的字面值0b_10000_10_11是不正确的。你不能在用于定义二进制字面值的0b0B前缀之后放置下划线。正确的值是0b10000_10_11

var5定义的字面值0xa10_AG_75是不正确的,因为它使用了字母G,这在十六进制数系统中是不允许的。正确的值是0xa10_A_75

var1定义的字面整数值是有效的。但0(用于八进制字面值)是规则的一个例外,即基数前缀不能被下划线孤立(例如,0x_100_267_7600b_100_110是无效的表达式)。

A.2.3 故事中的转折 2.2

目的:为了加强以下概念:

  • 可以在同一行代码中定义多个相同类型的变量。

  • 变量赋值规则:如果同一行上为多个相似类型的变量赋值,赋值从右向左开始。此外,与 C 语言等其他编程语言不同,字面值0不能赋给类型为boolean的变量。

  • 要求你选择错误答案或代码的问题可能会令人困惑。通常,我们首先确定错误的选项,然后选择正确的选项。注意这些问题。

答案:a, b, c, e

说明:选项(a)和(b)是错误的陈述。你可以在同一行上定义多个相同类型的变量。此外,你还可以在同一行代码中为兼容类型的变量赋值。赋值从右向左开始。为了证明这一点,以下代码行将可以编译:

int int1;
long long2;
long2 = int1 = 10;

但以下代码行将无法编译:

int i1;
long l2;
int1 = long2 = 10;

在前面代码的最后一行中,字面值10被赋值给类型为long的变量long2,这是可接受的。尝试将变量long2的值赋给int1会失败,因为这需要显式的转换。

选项 (c) 是一个错误的陈述,因为字面值 0 不能赋值给 boolean 类型的变量。

选项 (d) 是一个正确的陈述。

选项 (e) 是一个错误的陈述。代码没有定义名为 yes 的变量,因此似乎将其视为字面值。Java 没有定义字面值 yes,所以代码无法编译。

A.2.4 故事中的转折 2.3

目的:这个练习鼓励你

  • 尝试使用增量、减量后缀和前缀一元运算符的代码

  • 熟悉变量在具有后缀和前缀表示法中多个一元运算符的表达式中的评估方式。

答案:32

说明:实际任务是评估以下表达式:

int a = 10;
a = ++a + a + --a - --a + a++;
System.out.println(a);

这是实际的任务,因为问题要求你将所有 ++a 替换为 a++, --a 替换为 a--,反之亦然。这个表达式在图 A.1 中展示:

图 A.1. 具有多重一元运算符(后缀和前缀表示法)的表达式评估

图片

A.2.5 故事中的转折 2.4

目的:确定使用短路运算符 &&|| 的表达式的操作数是否会评估。

答案:将要执行的操作数用圆圈标出,不会执行的操作数用矩形框起来,见图 A.2。

图 A.2. 在使用短路运算符 &&|| 的表达式中,被评估的操作数用圆圈标出,未被评估的操作数用矩形框起来。

图片

说明:短路运算符 &&|| 都会评估它们的第一操作数。对于短路运算符 &&,如果第一个操作数评估为 false,则不会评估第二个操作数。对于短路运算符 ||,如果第一个操作数评估为 true,则不会评估第二个操作数。

对于表达式 (a++ > 10 || ++b < 30),因为 a++ > 10 评估为 false,两个操作数都将被评估。

对于表达式 (a > 90 && ++b < 30),因为 a > 90 评估为 false,第二个操作数不会执行。

对于表达式 (!(c > 20) && a == 10),因为 !(c > 20) 评估为 false,第二个操作数不会执行。

表达式 (a >= 99 || a <= 33 && b == 10) 有三个操作数,以及 OR (||) 和 AND (&&) 短路运算符。因为短路运算符 AND 的运算符优先级高于短路运算符 OR,所以表达式按以下方式评估:

(a >= 99 || (a <= 33 && b == 10))

评估前面的表达式从评估 (a <= 33 && b == 10) 开始。因为 a <= 33 评估为 true,运算符 && 会评估第二个操作数 (b == 10) 以确定 (a <= 33 && b == 10) 将返回 true 还是 falsea <= 33 返回 true,而 b == 10 返回 false,所以表达式 (a <= 33 && b == 10) 返回 false

原始表达式—(a >= 99 || (a <= 33 && b == 10))—现在简化为以下表达式:

(a >= 99 || false)

短路操作符 OR (||)执行其第一个操作数(即使第二个操作数的值已知),评估a >= 99。所以对于这个表达式,所有三个操作数都被评估。

表达式(a >= 99 && a <= 33 || b == 10)也有三个操作数,以及ORAND短路操作符。因为短路操作符AND的运算符优先级高于短路操作符OR,所以这个表达式按以下方式评估:

((a >= 99 && a <= 33) || b == 10 )

a >= 99评估为false,所以下一个操作数(a <= 33)不会被评估。因为操作符||的第一个操作数(a >= 99 && a <= 33)评估为false,所以评估b == 10

A.3 第三章:方法和封装

第三章包括三个故事中的转折练习。

A.3.1 故事中的转折 3.1

目的:与这个练习中定义与其实例变量同名局部变量的TestPhone类一样,我强烈建议你尝试在类中定义具有相同名称但不同作用域的变量的不同组合。

答案:a

说明:类Phone定义了一个名为phoneNumber的实例变量。方法setNumber也定义了一个局部变量phoneNumber并将其局部变量的值赋给它。局部变量会覆盖类中具有相同名称的实例变量。因为实例变量phoneNumber的值没有变化,所以123456789被打印到TestPhone类中定义的main方法控制的控制台。

A.3.2 故事中的转折 3.2

目的:了解不允许对构造函数进行递归循环调用。

答案:代码无法编译,出现以下编译错误信息:

Employee.java:4: error: recursive constructor invocation
    Employee() {
    ^
1 error

说明:一个方法调用自身被称为递归。两个或更多方法以循环方式相互调用被称为循环方法调用

从 Java 版本 1.4.1 开始,Java 编译器不会编译具有递归循环构造函数的代码。构造函数用于初始化对象,因此允许对构造函数进行递归调用是没有意义的。你可以初始化对象一次,然后修改它。你不能多次初始化对象。

如果你想知道是否可以从另一个构造函数有条件地调用构造函数,你无法这样做。对构造函数的调用必须是第一条语句:

此外,不允许循环构造函数调用:

之前的例子无法编译,出现以下编译错误信息:

Employee.java:8: error: recursive constructor invocation
    Employee(String newName, int newAge) {
    ^
1 error

注意,在方法中定义的类似的递归或循环调用不会导致编译错误。

A.3.3 故事中的转折 3.3

目的:具有public实例变量(s)的类永远不能被指定为良好封装的类。

答案:e

说明:这个问题试图通过定义玩弄方法 getWeightsetWeight 的多个访问修饰符的选项来欺骗你。因为类 Phone 的实例变量 model 使用 public 访问修饰符定义(并且没有提出的选项解决这个问题),它可以在类外访问。所以 Phone 不是一个封装良好的类。

A.4 第四章:Java API 中的选择类和数组

第四章 包含四个“故事中的转折”练习。

A.4.1 “故事中的转折”4.1

目的:提醒你注意类 String 的重载方法,这些方法可以接受 charString 或两者,这个练习中的代码向 startsWith 方法传递了一个无效的方法参数——一个 char

答案:e

说明:当涉及到 String 类时,很容易混淆接受 charString 值作为方法参数的方法。例如,重载的方法 indexOf 可以接受 Stringchar 值来在 String 中搜索目标值。方法 startsWithendsWith 只接受 String 类型的参数。方法 charAt 只接受 int 类型的参数。因此,这个方法可以传递 char 值,这些值被存储为无符号整数值。

A.4.2 “故事中的转折”4.2

目的:这个练习有多个目的:

  • 为了让你混淆方法名称的使用,这些名称被其他类在 Java API 中用来创建它们的对象。

  • 为了鼓励你在使用 Java API 中的类时参考 Java API 文档。Java API 文档是一个广泛的信息和事实来源,这些信息通常不包括在大多数书中(因为实际上不可能做到这一点)。

答案:d

说明:创建具有默认容量 16 个字符的 StringBuilder 类对象的正确方法是调用 StringBuilder 的无参数构造函数,如下所示:

StringBuilder name = StringBuilder();

A.4.3 “故事中的转折”4.3

目的:识别未初始化的数组元素和不存在数组元素之间的区别。一个多维数组的图示很容易绘制,你可以轻松地参考其不存在或 null 的数组元素。这个概念在 图 A.3 中展示。

图 A.3. 数组 multiStrArr 及其元素

图 A.3

答案:b, d

说明:选项(a)是错误的。使用 {"Jan","Feb",null}{"Jan","Feb",null,null} 初始化数组 multiStrArr 的行并不相同。前者定义了 三个 数组元素,最后一个数组元素被赋值为 null。后者定义了 四个 数组元素,最后两个数组元素被赋值为 null

选项(b)是正确的。该位置的数组元素存在,但没有被赋予任何值。它被赋值为 null

选项(c)是错误的。因为 multiStrArr[1] 指向 null,所以 multiStrArr[1][1] 不存在。

选项 (d) 是正确的。如图 A.3 所示,数组 multiStrArr 在每一行中定义的元素数量不相等,因此它是不对称的。

A.4.4 故事中的转折 4.4

目的:这个练习试图通过使用多个 ArrayList 对象、将一个 ArrayList 的对象引用赋给另一个,并修改 ArrayList 对象的值来欺骗你。String 对象是不可变的——你不能改变它们的值。

答案:a

解释:选项 (a) 是正确的,而选项 (b)、(c) 和 (d) 是错误的。ArrayList myArrListyourArrList 包含 String 对象。一旦创建,String 对象的值就不能修改。

A.5 第五章:流程控制

第五章 包含四个故事中的转折练习。

A.5.1 故事中的转折 5.1

目的:为了强调多个要点:

  • 任何类型的变量都可以在 if 条件中使用的表达式中(重新)赋值。

  • if-else-if 语句在控制传递给它们时执行每个 if 条件,并更改在表达式评估中操作的任何变量的值。

  • 用于 if 条件的表达式应该评估为 boolean 值。

答案:f

解释:本练习中代码语句的执行流程如图 A.4 所示。

图 A.4. 故事中的转折 5.1 中的代码执行流程

图 A.4

图 A.4 中左侧的箭头显示了此代码片段中语句的执行流程。右侧的 if 条件显示了在 if 语句中使用的表达式评估后的实际比较值。以下是一个详细的描述:

  • 变量 score 的初始值是 10。第一个条件 ((score = score + 10) == 100) 将变量 score 的值重新赋为 20,然后将其与字面整数值 100 进行比较。表达式 20 == 100 返回 booleanfalse。控制不会评估 if 构造的 then 部分,而是继续评估 else 部分中定义的第二个 if 条件。

  • 第二个条件 ((score = score + 29) == 50)29 添加到变量 score 的现有值 20 上,然后将新值 4950 进行比较。表达式 49 == 50 再次返回 false。控制不会评估 if 构造的 then 部分,而是继续评估 else 部分中定义的第二个 if 条件。

  • 第三个条件((score = score + 200) == 10)200的值加到变量score现有的49值上,使其变为249,并将其与整型字面值10进行比较。因为249 == 10评估为false,控制流移动到else部分。else部分将字面值F赋给变量result。在if-else-if语句执行结束时,变量score被赋予249的值,result被赋予F的值。代码输出F:249

考试技巧

这个练习是一个很好的机会来提醒你,这样的赋值总是在它们所属的测试或其他表达式之前执行(即预赋值),除了后增量(即后缀++)。

A.5.2 故事中的转折 5.2

目的:switch结构使用equals方法比较其参数的值与case值。它不比较变量引用。

答案:c

说明:你可能已经用如下代码回答过问题,该代码打印false

String aDay = new String("SUN");
System.out.println(aDay == "SUN");

使用赋值运算符(=)创建的String对象存储在String对象池中,但使用new运算符创建的String对象不存储在String对象池中。

当一个String对象作为参数传递给switch结构时,它不比较对象引用;它使用equals方法比较对象值。在问题中显示的代码片段中,找到了与String字面值SUN的匹配,因此代码打印Weekend!,执行break语句,并退出块。

A.5.3 故事中的转折 5.3

目的:注意传递给switch结构的变量的类型。在原始数据类型中,你可以将byteshortcharint类型的变量传递给switch结构。可以传递给switch结构的其他数据类型有ByteShortIntegerCharacterenumString

这个问题试图将你的注意力从这个简单的基本要求转移到问题的逻辑上。

答案:哈里的提交。

说明:保罗的提交无法编译,因为switch结构不接受long原始数据类型的参数。

A.5.4 故事中的转折 5.4

目的:当在嵌套循环(任何组合的fordo-whilewhile循环)中使用未标记的break语句时,break语句将结束inner loop的执行,而不是所有嵌套循环。outer loop将继续执行,从它的下一个迭代值开始。

答案:a

说明:让我们从outer loop的第一次迭代开始。在第一次迭代中,变量outer的值是Outer

对于外层循环的第一次迭代,内层循环应该对变量 inner 的值 OuterInner 执行。对于内层循环的第一次迭代,变量 inner 的值是 Outer,所以条件 inner.equals("Inner") 评估为 falsebreak 语句不执行。代码打印变量 inner 的值,即 Outer:,并开始内层循环的下一个迭代。在内层循环的第二次迭代中,变量 inner 的值是 Inner,所以条件 inner.equals("Inner") 评估为 truebreak 语句执行,结束内层循环的执行,跳过打印变量 inner 值的代码。

外层循环从第二次迭代开始执行。在这个迭代中,变量 outer 的值是 Outer。对于外层循环的迭代,内层循环以与上一段中提到相同的方式执行两次。外层循环的这个迭代再次打印变量 inner 的值,当它等于 Outer 时。

问题中包含的嵌套循环打印出 Outer: 两次:

Outer:Outer:

A.6 第六章:使用继承

第六章 包含了四个“故事中的转折”练习。

A.6.1 “故事中的转折”6.1

目的:这个问题是一个简单概念(private 成员对派生类不可访问)的例子,通过包含试图转移您注意力的代码和选项,使其看起来很复杂。考试中可能会出现类似的问题。

答案:e

说明:代码未能编译,因为类中的 private 成员不能在类外访问——甚至不能由其派生类访问。编译器可以检测此类尝试;此代码无法编译。

A.6.2 “故事中的转折”6.2

目的:帮助您处理组合

  • 数组

  • 将派生类的对象赋给基类的引用变量

  • 将实现接口的类的对象赋给接口的引用变量

答案:a, c

说明:您需要遵循的规则来为数组元素赋值与您在将对象赋给引用变量时遵循的规则相同。因为数组 interviewer 的类型是 Interviewer,您可以分配实现此接口的类的对象。EmployeeManagerHRExecutive 类以及接口 Interviewer 的继承关系如 图 A.5 所示。

图 A.5. 类 EmployeeManagerHRExecutive 以及接口 Interviewer 的继承层次结构的 UML 表示

如您在 图 A.5 中所见,类 ManagerHRExecutive 实现了接口 Interviewer。类 Employee 没有实现接口 Interviewer;因此,Employee 类的对象不能添加到类型为 Interviewer 的数组中。

从这个解释中可以看出,选项(a)和(c)是正确的,选项(b)是不正确的。

选项(d)是不正确的,因为你不能创建接口的对象。选项(d)试图创建接口Interviewer的对象。试图创建接口实例的代码将无法编译。

A.6.3 故事中的转折 6.3

目的:如果没有与基类或派生类中定义的变量名称冲突,则可以从派生类使用superthis引用访问变量。如果有冲突,则可以使用super引用来访问基类变量。

答案:b

说明:在派生类中,你通常使用隐式引用super来引用基类的方法或变量。同样,你通常使用隐式引用this来引用同一类中定义的方法或变量。派生类包含其基类的对象,并且可以访问其基类的非私有成员。派生类可以使用引用this来将其基类的成员作为自己的成员来引用。这种方法只有在派生类中没有定义相同的成员时才是可接受的,也就是说,没有名称冲突。

基类Employee定义了两个非私有变量,nameaddress,这些变量在Employee的派生类Programmer中是可访问的。类Programmer还定义了一个实例变量name,因此变量name应该使用显式的引用superthis来引用在EmployeeProgrammer类中定义的变量name。变量address在派生类Programmer中可以使用superthis来引用。

选项(a)是不正确的。派生类Programmer可以使用this.address来引用在基类中定义的变量address。这个值不会打印null

选项(c)是不正确的。从派生类Programmer访问时,this.address不会打印空白。

选项(d)是不正确的。代码没有编译问题。

A.6.4 故事中的转折 6.4

目的:多态方法应该定义一个方法的重写规则。

答案:a

说明:当类或接口共享继承关系时存在多态方法。如果派生类可以定义一个多态方法,则

  • 派生类实现了在基类或接口中定义的抽象方法

  • 派生类覆盖了在基类中定义的非抽象方法

选项(b)和(d)是不正确的。如果一个方法定义了不同的参数列表,则不能重写该方法。

选项(c)是不正确的。重写方法的重写类型必须在基类和派生类中相同。

A.7 第七章:异常处理

第七章 包含了五个故事中的转折练习。

A.7.1 故事中的转折 7.1

目的:finally 块不能放在 catch 块之前。许多程序员将这个问题与在 switch 结构中将标签 default 放在标签 case 之前进行比较。尽管后者方法可行,但 finallycatch 块并不那么灵活。

答案:d

解释:选项 (a)、(b) 和 (c) 是错误的,因为定义在 catch 块之前的 finally 块的代码无法编译。

A.7.2 故事中的转折 7.2

目的:未处理的异常由内部异常处理器传递到外部的 try-catch 块以处理。

答案:a

解释:选项 (b)、(c) 和 (d) 是错误的。此问题假设系统上存在一个名为 players.txt 的文本文件,因此以下代码不会抛出 FileNotFoundException 异常:

players = new FileInputStream("players.txt");

为此问题定义的代码在执行以下代码之前没有初始化 static 变量 coach,这将必然抛出 NullPointerException

coach.close();

上一行代码定义在内部 try 块中,该块没有为 NullPointerException 异常定义异常处理器。这个异常被传播到外部的异常处理器块。外部异常处理器 捕获 内部 try 块抛出的 NullPointerException 并执行适当的异常处理器。因此,代码打印以下内容:

players.txt found
NullPointerException

A.7.3 故事中的转折 7.3

目的:确定错误处理的异常代码是否会执行。

答案:b

解释:我们知道通常错误不应该通过程序处理,而应该留给 JVM 处理。此外,你也不能确定所有错误的错误处理代码都会执行。例如,StackOverFlowError 的错误处理代码可能会执行,但(正如其名称所暗示的)可能不会为 VirtualMachineError 执行。

A.7.4 故事中的转折 7.4

目的:ClassCastException 是一个运行时异常。正如你所知,运行时异常只能由 JVM 抛出。

答案:b, d

解释:选项 (a) 和 (c) 是错误的,因为代码会抛出 ClassCastException,这是一个运行时异常,对于以下代码:

printable = (Printable)blackInk;

选项 (d) 是正确的,因为 BlackInk 类及其任何基类都没有实现 Printable 接口。因此,将 blackInk 赋值给 printable 而不进行显式转换的代码将无法编译。

A.7.5 故事中的转折 7.5

目的:尝试访问数组中不存在的位置会抛出 ArrayIndexOutOfBoundsException。在存储在数组中的 null 值上调用成员会抛出 NullPointerException

答案:c

解释:让我们缩进二维数组 oldLaptops 的赋值,以便更容易理解分配给它的值:

String[][] oldLaptops = {
                          {"Dell", "Toshiba", "Vaio"},
                           null,
                          {"IBM"},
                           new String[10]
                        };

之前的代码导致以下赋值:

oldLaptops[0] = {"Dell", "Toshiba", "Vaio"};
oldLaptops[1] = null;
oldLaptops[2] = {"IBM"};
oldLaptops[3] = new String[10];

两个维度的 String 数组 oldLaptops 的示意图显示在 图 A.6 中。

图 A.6. 数组 oldLaptops

如您所见,oldLaptops[3] 是一个包含 10 个未初始化的 String 对象的数组。数组 oldLaptops[3] 的所有成员(从索引位置 09)都被分配了一个 null 值。第 4 行的代码试图在数组 oldLaptops[0] 的第一个元素上调用 length 方法,该元素是 null,从而抛出了 NullPointerException

posted @ 2025-11-17 09:51  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报