Oracle-专业认证-JavaSE8-编程测验-全-
Oracle 专业认证 JavaSE8 编程测验(全)
一、OCPJP8 考试:常见问题
首字母缩写词 OCPJP 8 考试代表 Java SE 8 程序员 II 考试(考试编号 1Z0-809)。在第一章中,我们将讨论在你准备 OCPJP 八级考试时可能出现的常见问题。
概观
常见问题 1。你能提供 Java 8 的 Java 助理和专业考试的细节吗?
OCAJP 8 考试(Oracle Certified Associate Java Programmer certification,考试编号 1Z0-808)主要面向入门级 Java 开发人员。当你通过了这个考试,就说明你有很强的 Java 基础。
OCPJP 8 考试(甲骨文认证专业 Java 程序员认证,考试编号 IZ0-809)是为专业 Java 开发人员准备的。当你通过了这个考试,就证明了你可以在日常工作中使用广泛的核心 Java 特性(尤其是 Java 8 中增加的特性)。
常见问题 2。你能比较一下 OCAJP 八级和 OCPJP 八级认证考试的规格吗?
是的,参见表 1-1 。
表 1-1。
Comparison of the Oracle Exams Leading to OCAJP8 and OCPJP8Certifications
考试编号 | 1Z0-808 | 1Z0-809 | 1Z0-810 | 1Z0-813 |
---|---|---|---|---|
专业水平 | 新手 | 中间的 | 中间的 | 中间的 |
考试名称 | Java SE 8 程序 I | Java SE 8 Programs II | 将 Java SE 7 升级到 Java SE 8 OCP 程序员 | 升级到 Java SE 8 OCP 版(Java SE 6 和所有早期版本) |
相关认证(缩写) | 甲骨文认证助理,Java SE 8 程序员(OCAJP 8) | Oracle 认证专家,Java SE 8 程序员(OCPJP 8) | Oracle 认证专家,Java SE 8 程序员(升级)(OCPJP 8) | Oracle 认证专家,Java SE 8 程序员(OCPJP 8) |
先决条件认证 | 没有人 | Java SE 8 Programs I (OCAJP8) | Java SE 7 程序员 II (OCPJP 7) | Oracle Certified Professional Java SE 6 程序员和所有早期版本(OCPJP 6 和早期版本) |
考试持续时间 | 2 小时 30 分钟(150 分钟) | 2 小时 30 分钟(150 分钟) | 2 小时 30 分钟(150 分钟) | 2 小时 10 分钟(130 分钟) |
问题数量 | 77 个问题 | 85 个问题 | 81 个问题 | 60 个问题 |
通过百分比 | 65% | 65% | 65% | 63% |
费用 | 245 美元 | ∼245 美元 | ∼245 美元 | ∼245 美元 |
考试主题 | Java 基础知识使用 Java 数据类型使用运算符和决策构造创建和使用数组使用循环构造使用方法和封装使用继承处理异常使用 Java API 中的选定类 | Java 类设计高级 Java 类设计泛型和集合 Lambda 内置函数式接口 Java 流 API 异常和断言使用 Java SE 8 日期/时间 API Java I/O 基础 Java 文件 I/O (NIO.2) Java 并发性使用 JDBC 本地化构建数据库应用 | 使用内置 Lambda 类型的 Lambda 表达式使用 Lambda 过滤集合使用 Lambda 并行流的 Lambda 集合操作 Lambda Cookbook 方法增强使用 Java SE 8 日期/时间 API | 语言增强并发本地化 Java 文件 I/O (NIO.2) Lambda Java 集合 Java 流 |
笔记
- 在费用行中,给定的考试美元费用是近似值,因为实际费用随您参加考试的国家的货币而变化:美国 245 美元,英国 155 美元。印度的 9604 等。
- 考试主题行仅列出顶级主题。对于子主题,请查看甲骨文网页上这些考试。
- 此处提供的详细信息截至 2015 年 11 月 1 日。请查看甲骨文网站,了解考试详情的任何更新。
关于考试的细节
常见问题 3。OCAJP 8 认证是 OCPJP 8 认证的先决条件。那是不是说我要先考 OCAJP8 才能考 OCPJP8?
不,认证要求可以按任何顺序满足。您可以在参加 OCAJP 8 级考试之前参加 OCPJP 8 级考试,但是在您通过 1z 0-808 和 1Z0-809 考试之前,您不会被授予 OCPJP 8 级认证。
常见问题 4。OCPJP 八级考试与旧的 OCPJP 七级考试有什么不同?
与 OCPJP 7 考试中的考试主题相比,OCPJP 8 考试更新了 Java SE 8 版本中添加的主题:lambda 函数、Java 内置函数式接口、流 API(包括并行流)、日期和时间 API,以及对 Java 库的其他更改。
常见问题 5。我应该参加 OCPJP8 考试还是早期版本如 OCPJP 7 考试?
虽然你仍然可以参加旧认证的考试,如 OCPJP 7,OCPJP 8 是最好的专业证书,因为它是根据最新的 Java SE 8 版本进行验证的。
常见问题 6。在 OCPJP 八级考试中会问什么样的问题?
OCPJP 8 考试中的一些问题测试你的概念性知识,而不涉及具体的程序或代码段。但是大多数问题是以下类型的编程问题:
- 给定一个程序或代码段,输出或预期行为是什么?
- 哪个(些)选项可以编译而不出错或者给出期望的输出?
- 哪些选项构成了给定 API 的正确用法(特别是新引入的 API,如流和日期/时间 API)?
所有问题都是选择题。大部分都呈现四五个选项,但有些有六七个选项。许多问题被设计成具有多个正确答案的集合。这类问题明确提到了你需要选择的选项数量。
考试问题并不局限于考试大纲中的主题。例如,你可能会收到关于 Java 基础知识(来自 OCAJP 课程大纲)的问题,涉及异常处理和使用包装器类型的基础知识。你也可能会遇到与考试大纲相关但没有明确说明的问题。例如,在考试中,你可能会得到一个关于java.util.function.BinaryOperator
接口的问题,尽管“Java 内置函数式接口”考试主题没有明确提到这个接口。
给定的问题并不局限于只测试一个主题。有些问题旨在用一个问题测试多个主题。例如,您可能会发现一个关于并行流的问题,它利用了内置的函数式接口和 lambda 表达式。
常见问题 7。OCPJP 八级考试考什么?
OCPJP 8 考试测试你对开发真实世界程序所必需的 Java 语言特性和 API 的理解。考试重点关注以下几个方面:
- 对解决问题有用的语言概念:考试不仅测试你对语言功能如何工作的知识,还包括你对语言功能的本质和关键情况的掌握。例如,您不仅需要理解 Java 中的泛型特性,还需要理解与类型擦除、混合遗留容器和泛型容器等相关的问题。
- Java APIs:该考试测试您对使用 Java 类库的熟悉程度,以及不寻常的方面或极限情况,如下所示:
java.util.function.Supplier
的二进制等价是什么?(回答:因为一个Supplier
不接受任何参数,所以对于Supplier
接口来说没有二进制等价物)。- 如果你尝试多次使用一个流会发生什么?(答:一旦在流上调用了一个终端操作,就认为已经使用或关闭;任何重用流的尝试都将导致抛出一个
IllegalStateException
。)
- 基本概念:例如,考试可能会测试您对序列化如何工作、重载和重写之间的差异、自动装箱和取消装箱如何与泛型相关、线程的不同类型的活动问题、并行流如何在内部使用 fork/join 框架等的理解。
虽然该考试不测试记忆技能,但有些问题假设关键要素的死记硬背知识,如以下内容:
java.util.function
包中关键功能接口提供的抽象方法的名称(Predicate
的"test"
方法、Consumer
的"accept"
方法、Function
的"apply"
方法、Supplier
接口的"get"
方法)。- 在
java.util.stream.Stream
接口及其原语类型版本中,需要记住常用的中间操作和终端操作的名称。
常见问题 8。在过去的五年里,我一直是一名 Java 程序员。我必须准备 OCPJP 八级考试吗?
简而言之:你有工作经验很好,但你仍然需要准备 OCPJP 八级考试。
长的回答:不管你有多少真实世界的编程经验,有两个原因你应该准备这个考试来增加你通过考试的机会:
- 你可能没有接触过考试中的某些话题。Java 很庞大,你可能没有机会研究考试中涉及的每一个主题。例如,如果您从未处理过您所从事的应用的地区方面,您可能不熟悉本地化。或者你的工作可能不需要你使用 JDBC。或者您一直在单线程程序上工作,所以多线程编程对您来说可能是新的。此外,OCPJP8 强调 Java 8,您可能还没有接触过诸如 lambda 表达式、顺序和并行流、日期和时间 API 以及内置函数式接口等 Java 8 主题。
- 你可能不记得不寻常的方面或角落的情况。不管你有多有经验,当你编程的时候总会有惊喜的成分。OCPJP8 考试不仅测试你在常规功能方面的知识和技能,还测试你对不寻常方面或极端情况的理解,如多线程代码的行为和涉及重载和重写时泛型的使用。所以你必须钻研工作中很少遇到的病理病例。
常见问题 9。我该如何准备 OCPJP 八级考试?
研究这本书。此外,
- 代码,代码,代码!写很多很多小程序,用它们做实验,从你的错误中学习。
- 读,读,读!请阅读本书以及 Oracle 网站上的教程和参考资料,尤其是
- 甲骨文免费在线 Java 教程:在
http://docs.oracle.com/javase/tutorial/
访问 Java 教程。 - Oracle 的 Java 8 central:您可以下载最新的 Java SDK,获得访问 Java SE 社区的链接,从该页面阅读关于 Java 8 的免费技术文章:
http://www.oracle.com/technetwork/java/javase/overview/java8-2100321.html
- Java 文档:Java API 文档是一个信息宝库。该文档可在线获得(参见
http://docs.oracle.com/javase/8/docs/api/
),并作为 Java SDK 的一部分提供。如果您不能立即访问互联网,您可能会发现 javac 的-Xprint
选项很方便。例如,要打印String
类的文本表示,键入完全限定名,如javac -Xprint java.lang.String
- 甲骨文免费在线 Java 教程:在
这将在控制台上打印出String
类的成员列表。
- 读,编码,读,编码!在你的阅读和编码之间来回循环,这样你的书本知识和它的实际应用是相互加强的。这样,你不仅会知道一个概念,还会理解它。
- 把注意力集中在你最不习惯的话题上。从 1 到 10,对 OCPJP 八级考试的每一个题目给自己打分。对所有你给自己打 8 分或更低的题目做补救准备。
常见问题 10。我如何知道我什么时候准备好参加 OCPJP 八级考试?
在实际考试条件下参加第十四章中的模拟考试:坚持 2.5 小时的时间限制;不要休息,也不要查阅任何书籍或网站。如果你的分数达到 65%或以上(这是 1Z0-809 考试的及格分数),你就有可能通过实际考试。
参加考试
常见问题 11。我报名参加考试有哪些选择?
OCPJP 八级考试有三个注册选项:
- 在皮尔逊 VUE 网站注册并付款。
- 从甲骨文购买考试券,然后在皮尔逊 VUE 网站注册。
- 如果您所在地区有甲骨文测试中心(OTC ),请在那里注册并付款。
常见问题 12。我如何报名参加考试,安排参加考试的日期和时间,并参加考试?
选项 1:使用以下步骤在培生 VUE 网站上注册并付款:
- 第一步。转到
www.pearsonvue.com/oracle/
(如果您在 Oracle 认证页面中单击第一个选项,将被引导到此处)。点击“安排考试”部分的“在线安排”。 - 第二步。选择“登录”在“您计划参加的考试类型”部分点击“监考”。选择本次检查为
"Information Technology (IT)" ➤ "Oracle" ➤ "Proctored"
。然后你会被要求签到。 - 第三步。登录您在 Pearson 网站上的 web 帐户。如果你没有,那就创建一个;您将通过提供的电子邮件获得用户名和密码。首次登录时,您需要更改密码并设置安全问题及其答案。当你完成这些后,你就可以安排考试了。
- 第四步。登录后,您将获得可供选择的 Oracle 考试列表。选择以下考试:这些考试使用英语(如果您愿意,并且列表中有其他语言,您可以选择其他语言)。这个页面还会显示考试的费用。点击
Next
。- 1Z0-809,Java SE 程序员 II(又名 OCPJP 8 考)These exams are in English (You can choose another language if you wish and if it is available in the list). This page will also show you the cost of the exam. Click
- 第五步。现在您需要选择您的测试位置。选择
Country ➤ City ➤ State/Province
,将显示您附近的测试位置。每个中心将有一个信息图标:点击它的地址和方向。选择您所在位置附近的最多四个中心,然后点击Next
。 - 第六步。选择一个考试中心,并选择约会的日期和时间。该页面将显示可用的日期和时间段;选择一个对你来说最方便的。如果您有考试优惠券或甲骨文大学优惠券或甲骨文促销代码,请在此输入。
- 第七步。从可用的支付选项中选择(通常的方式是使用信用卡支付)并支付考试费。在付费之前,请确保您选择了正确的考试、合适的考试中心和日期/时间。
- 第八步。搞定了。您将通过电子邮件收到预约确认付款收据。
选项 2:从甲骨文公司购买考试优惠券,并在皮尔森 VUE 公司网站上注册。
您可以从 Oracle 购买通用考试优惠券,并在 Pearson 网站上使用。如果你住在美国,费用是 245 美元,如果你住在其他地方,费用以适当的货币计价。要从 Oracle 购买优惠券,请选择“OU Java、Solaris 和其他 Sun 技术考试 e voucher”如果您没有 Oracle 帐户,将要求您创建一个。创建账户后,确认客户类型、客户联系信息并付款。一旦你支付了费用,你就可以在皮尔逊 VUE 网站上使用 eVoucher。
选项 3:在甲骨文考试中心(OTC)注册并在线支付,亲自参加考试。
如果体检安排在您附近,您可以选择此选项。它的价格为 245 美元或当地等值货币。
常见问题 13。考试前和考试当天我需要记住的重点是什么?
考试前一天:
- 您将收到皮尔逊的电子邮件,确认您的预约和付款。当你去考试中心的时候,检查你应该带什么的细节。注意,你至少需要两张带照片的身份证。
- 考试前,你会接到你预约的皮尔森考试中心的电话。
考试当天:
- 考试开始前至少 30 分钟到考点。你的考试中心将有储物柜存放你的物品。
- 出示您的考试日程信息和身份证,然后完成考试手续,如签署文件。
- 你将被带到考场的一台电脑前,然后登录考试软件。
参加考试:
- 您将在考试软件屏幕上看到以下内容:
- 在一个角落里显示剩余时间的计时器
- 您正在尝试的当前问题编号
- 如果您想稍后查看问题,请选中此复选框
- 按钮(标有“复习”)用于进入复习屏幕,在完成考试之前,您可以在这里重新复习问题。
- 一旦开始,你会看到一个接一个的问题。您可以通过在复选框中选择答案来选择答案。如果您不确定答案,请选择标记按钮,以便在考试过程中随时重新查看。您也可以右键单击某个选项来删除该选项(这对于消除不正确的选项很有用)。
- 考试期间,你不得咨询任何人,也不得查阅印刷或电子材料或程序。
考试结束后:
- 一旦你完成了考试,你不会立即看到结果。你必须登录甲骨文的 CertView 网站(
https://education.oracle.com/certview.html
)才能看到考试成绩。 - 不管考试是否通过,你答错的题目都会和你的分数一起提供。
- 如果您已经通过了 OCPJP 8 考试,并且也满足了适用的认证先决条件(例如,通过 1Z0-809 考试将 OCAJP 8 认证作为 OCPJP 8 认证的先决条件),一份可打印的证书将通过电子邮件发送给您。
- 如果您没有通过考试,您可以注册并再次付费,在 14 天的等待期后重考。
二、Java 类设计
认证目标 |
---|
实现封装 |
实现继承,包括可见性修饰符和组合 |
实现多态性 |
重写 Object 类的 hashCode、equals 和 toString 方法 |
创建和使用单例类和不可变类 |
开发在初始化块、变量、方法和类时使用 static 关键字的代码 |
面向对象(OO)是当今大多数主流编程语言的核心。为了创建高质量的设计和软件,很重要的一点是要掌握面向对象的概念。这一章是关于类设计的,下一章是关于高级类设计的,这为你用 Java 创建高质量的设计打下了坚实的基础。
由于 OCAJP 8 是 OCPJP 8 考试的先决条件,我们假设你熟悉基本概念,如方法,字段,以及如何定义一个构造函数。因此,在这一章中,我们开始直接讨论 OCPJP 8 考试题目。在第一部分中,我们讨论了如何使用访问说明符实施封装,以及如何实现继承和多态。在下一节中,我们将深入研究在Object
类中覆盖方法的细节,定义单例类和不可变类,并分析使用static
关键字的不同方式。
包装
认证目标 |
---|
实现封装 |
结构化编程将程序的功能分解成不同的过程(函数),而不太关心每个过程可以处理的数据。函数可以自由地操作和修改(通常是全局的和无保护的)数据。
在面向对象编程(OOP)中,数据和相关联的行为形成了一个单一的单元,称为类。术语封装是指将数据和相关功能组合成一个单元。例如,在一个Circle
类中,radius
和center
被定义为私有字段。现在您可以添加方法,如draw()
和fillColor()
以及字段radius
和center
,因为字段和方法彼此紧密相关。该类中的方法所需的所有数据(字段)都可以在该类内部获得。换句话说,该类将其字段和方法封装在一起。
访问修饰符
认证目标 |
---|
实现继承,包括可见性修饰符和组合 |
访问修饰符决定了 Java 实体(类、方法或字段)的可见性级别。访问修饰符使您能够实施有效的封装。如果一个类的所有成员变量都可以从任何地方访问,那么就没有必要将这些变量放在一个类中,也没有必要将数据和行为封装在一个类中。
OCPJP 8 考试包括关于访问修饰语的直接问题和需要访问修饰语基础知识的间接问题。因此,理解 Java 支持的各种访问修饰符是很重要的。
Java 支持四种类型的访问修饰符:
- 公众
- 私人的
- 保护
- 默认值(未指定访问修饰符)
为了说明这四种类型的访问修饰符,让我们假设在一个绘图应用中有以下类:Shape, Circle, Circles
和Canvas
类。Canvas
级在appcanvas
包中,其他三级在graphicshape
包中(见清单 2-1 )。
Listing 2-1. Shape.java, Circle.java, Circles.java, and Canvas.java
// Shape.java
package graphicshape;
class Shape {
protected
int color;
}
// Circle.java
package graphicshape;
import graphicshape.Shape;
public
class Circle extends Shape {
private
int radius; // private field
public
void area() { // public method
// access to private field radius inside the class:
System.out.println("area: " + 3.14 * radius * radius);
}
// The fillColor method has default access
void fillColor() {
//access to protected field, in subclass:
System.out.println("color: " + color);
}
}
// Circles.java
package graphicshape;
class Circles {
void getArea() {
Circle circle = new Circle();
// call to public method area() within package:
circle.area();
// calling fillColor() with default access within package:
circle.fillColor();
}
}
// Canvas.java
package appcanvas;
import graphicshape.Circle;
class Canvas {
void getArea() {
Circle circle = new Circle();
circle.area(); // call to public method area(), outside package
}
}
公共访问修饰符
公共访问修饰符是最自由的。如果一个类或它的成员被声明为 public,那么不管包边界如何,它们都可以从任何其他类中被访问。它相当于现实世界中的公共场所,例如公司的自助餐厅,所有员工都可以使用,不管他们属于哪个部门。如清单 2-1 所示,Circle
类中的公共方法area()
可以在同一个包内访问,也可以在包外访问(在Canvas
类中)。
只有当一个类被声明为公共的,这个类中的公共方法才可以被外界访问。如果该类没有指定任何访问修饰符(即,它具有默认访问权限),那么该公共方法只能在包含它的包中访问。
私有访问修饰符
私有访问修饰符是最严格的访问修饰符。不能从类外部访问私有类成员;只有同一类的成员才能访问这些私有成员。它堪比银行里的保险箱室,只有一组授权人员和保险箱所有者才能进入。在清单 2-1 中,Circle
类的私有字段半径只能在Circle
类内部访问,而不能在任何其他类中访问,不管封装包是什么。
受保护和默认访问修饰符
受保护的和默认的访问修饰符彼此非常相似。如果成员方法或字段被声明为 protected 或 default,则可以在包内访问该方法或字段。请注意,没有显式关键字来提供默认访问;事实上,当没有指定访问修饰符时,该成员具有默认访问权限。另外,请注意,默认访问也称为受包保护的访问。受保护的和默认的访问类似于办公室中只有一个部门可以访问会议室的情况。
受保护的访问和默认访问有什么区别?当我们谈论一个子类属于另一个包而不是它的超类时,这两个访问修饰符之间的一个显著区别就出现了。在这种情况下,受保护的成员在子类中是可访问的,而默认成员则不是。
不能将类(或接口)声明为私有或受保护的。此外,接口的成员方法或字段不能声明为私有或受保护的。
在清单 2-1 中,受保护字段color
在类Circle
中被访问,默认方法fillColor()
从类Circles
中被调用。
表 2-1 总结了各种访问修饰符提供的可见性。
表 2-1。
Access Modifiers and Their Visibility
访问修饰符/可访问性 | 在同一个班级 | 包内的子类 | 包外的子类 | 包内的其他类 | 包外的其他类 |
---|---|---|---|---|---|
公众 | 是 | 是 | 是 | 是 | 是 |
私人的 | 是 | 不 | 不 | 不 | 不 |
保护 | 是 | 是 | 是 | 是 | 不 |
默认 | 是 | 是 | 不 | 是 | 不 |
遗产
继承是面向对象编程中的一种可重用机制。通过继承,各种对象的公共属性被用来形成彼此之间的关系。抽象和公共属性在超类中提供,超类可用于更专门化的子类。例如,彩色打印机和黑白打印机是打印机的种类(单一继承);一体式打印机是打印机、扫描仪和复印机(多重继承)。
为什么继承是一个强大的特性?因为它支持在一个层次结构中建模类,而且这样的层次模型很容易理解。例如,您可以从逻辑上将车辆分类为两轮车、三轮车、四轮车等等。在四轮车类别中,有轿车、货车、公共汽车和卡车。在汽车类别中,有掀背车、轿车和 SUV。当你分层分类时,理解、建模和编写程序就变得容易了。
考虑前面章节中使用的一个简单例子:类Shape
是一个基类,而Circle
是一个派生类。换句话说,一只Circle
就是一只Shape
;同样,一个Square
就是一个Shape
。因此,继承关系可以称为 IS-A 关系。
在 Java 库中,可以看到继承的广泛使用。图 2-1 显示了来自java.lang
库的部分继承层次。Number
类抽象出各种数值(引用)类型,如Byte
、Integer
、Float
、Double
、Short
和BigDecimal
。
图 2-1。
A partial inheritance hierarchy in java.lang package
类Number
有许多被派生类继承的公共方法。派生类不必实现由Number
类实现的公共方法。此外,您可以在需要基类型的地方提供一个派生类型。例如,Byte
是一个Number
,这意味着你可以在需要一个Number
对象的地方提供一个Byte
对象。为基类型编写方法时,可以编写通用方法(或算法)。清单 2-2 显示了一个简单的例子。
Listing 2-2. TestNumber.java
// Illustrates how abstracting different kinds of numbers in a Number hierarchy
// becomes useful in practice
public class TestNumber {
// take an array of numbers and sum them up
public static double sum(Number []nums) {
double sum = 0.0;
for(Number num : nums) {
sum += num.doubleValue();
}
return sum;
}
public static void main(String []s) {
// create a Number array
Number []nums = new Number[4];
// assign derived class objects
nums[0] = new Byte((byte)10);
nums[1] = new Integer(10);
nums[2] = new Float(10.0f);
nums[3] = new Double(10.0f);
// pass the Number array to sum and print the result
System.out.println("The sum of numbers is: " + sum(nums));
}
}
这个程序打印
The sum of numbers is: 40.0
在main()
方法中,您将nums
声明为一个Number[]
。一个Number
引用可以保存它的任何派生类型对象。您正在创建类型为Byte
、Integer
、Float
和Double
的对象,初始值为 10;nums
数组保存这些元素。(请注意,您需要在new Byte((byte) 10)
中进行显式强制转换,而不是普通的Byte(10)
,因为Byte
接受一个byte
参数,而 10 是一个int
。)
sum
方法接受一个Number[]
并返回Number
元素的总和。double
类型可以保存最大范围的值,所以使用double
作为sum
方法的返回类型。Number
有一个doubleValue
方法,这个方法返回由Number
保存的值作为double
值。for
循环遍历数组,添加double
值,然后在完成后返回sum
。
如您所见,sum()
方法是一个通用方法,可以处理任何Number[]
。从 Java 标准库中可以给出一个类似的例子,其中java.util.Arrays
类有一个静态方法binarySearch()
:
static int binarySearch(Object[] a, Object key, Comparator c)
这个方法在给定的数组Objects. Comparator
中搜索一个给定的键(一个Object
类型)是一个声明equals
和compare
方法的接口。您可以将binarySearch
用于实现这个Comparator
接口的任何类类型的对象。正如您所看到的,对于编写通用方法来说,继承是一个强大而有用的特性。
多态性
认证目标 |
---|
实现多态性 |
术语多态性的希腊词根指的是一个实体的“几种形式”。在现实世界中,你传达的每一条信息都有一个语境。根据上下文,消息的含义可能会改变,对消息的响应也可能会改变。类似地,在 OOP 中,根据对象的不同,消息可以有多种解释方式(多态性)。
多态有两种形式:动态和静态。
- 当单个实体的不同形式在运行时(后期绑定)被解析时,这种多态性被称为动态多态性。在上一节关于继承的内容中,我们讨论了重写。重写是运行时多态性的一个例子。
- 当单个实体的不同形式在编译时被解析时(早期绑定),这种多态性被称为静态多态性。函数重载是静态多态的一个例子,现在让我们来探讨一下。
请注意,抽象方法使用运行时多态性。我们将在下一章讨论接口中的抽象方法和抽象类(第三章——高级类设计)。
运行时多态性
您刚刚学习了基类引用可以引用派生类对象。您可以从基类引用中调用方法;然而,实际的方法调用取决于基类引用所指向的对象的动态类型。基类引用的类型称为对象的静态类型,运行时引用所指向的实际对象称为对象的动态类型。
当编译器从基类引用中看到方法调用时,并且如果该方法是可重写的方法(非静态和非最终方法),编译器会推迟确定要在运行时调用的确切方法(后期绑定)。在运行时,基于对象的实际动态类型,调用适当的方法。这种机制被称为动态方法解析或动态方法调用。
运行时多态性:一个例子
假设在Shape
类中有area()
方法。根据派生类——例如Circle
或Square
——area()
方法将被不同地实现,如清单 2-3 所示。
Listing 2-3. TestShape.java
class Shape {
public double area() { return 0; } // default implementation
// other members
}
class Circle extends Shape {
private int radius;
public Circle(int r) { radius = r; }
// other constructors
public double area() {return Math.PI * radius * radius; }
// other declarations
}
class Square extends Shape {
private int side;
public Square(int a) { side = a; }
public double area() { return side * side; }
// other declarations
}
public class TestShape {
public static void main(String []args) {
Shape shape1 = new Circle(10);
System.out.println(shape1.area());
Shape shape2 = new Square(10);
System.out.println(shape2.area());
}
}
这个程序打印
314.1592653589793
100.0
这个程序演示了如何基于Shape
的动态类型调用area()
方法。在这段代码中,语句shape1.area();
调用Circle's area()
方法,而语句shape2.area();
调用Square's area()
方法,从而得到结果。
现在,让我们问一个更基本的问题:为什么需要重写方法?在 OOP 中,继承的基本思想是在基类中提供一个默认的或公共的功能;派生类应该提供更具体的功能。在这个Shape
基类和Circle
和Square
派生类中,Shape
提供了area()
方法的默认实现。Circle
和Square
的派生类定义了覆盖基类area()
方法的area()
方法版本。因此,根据您创建的派生对象的类型,从基类引用,对area()
方法的调用将被解析为正确的方法。覆盖(即运行时多态性)是扩展功能的一个简单而强大的想法。
现在让我们讨论编译时多态性(重载)。在此之后,我们将立即回到运行时多态性的主题,讨论更多的主题,如当在组合和继承之间重写和选择时如何处理可见性修饰符。
方法重载
在一个类中,可以定义多少个同名的方法?很多!在 Java 中,只要参数列表互不相同,就可以用相同的名称定义多个方法。换句话说,如果您提供不同类型的参数、不同数量的参数,或者两者都提供,那么您可以用相同的名称定义多个方法。这个特性被称为方法重载。编译器将根据所传递参数的实际数量和/或类型来解析对正确方法的调用。
让我们在Circle
类中实现一个名为fillColor()
的方法,用不同的颜色填充一个圆形对象。当你指定一种颜色时,你需要使用一种配色方案,让我们考虑两种方案- RGB 方案和 HSB 方案。
When you represent a color by combining Red, Green, and Blue color components, it is known as RGB scheme. By convention, each of the color values is typically given in the range 0 to 255. When you represent a color by combining Hue, Saturation, and Brightness values, it is known as HSB scheme. By convention, each of the values is typically given in the range 0.0 to 1.0.
既然 RGB 值是整数值,HSB 值是浮点值,那么支持这两种方案调用fillColor()
方法怎么样?
class Circle {
// other members
public void fillColor (int red, int green, int blue) {
/* color the circle using RGB color values – actual code elided */
}
public void fillColor (float hue, float saturation, float brightness) {
/* color the circle using HSB values – actual code elided */
}
}
如您所见,两个fillColor()
方法有完全相同的名称,并且都有三个参数;但是,参数类型是不同的。基于在Circle
上调用fillColor()
方法时使用的参数类型,编译器将准确地决定调用哪个方法。例如,考虑以下方法调用:
Circle c1 = new Circle(10, 20, 10);
c1.fillColor(0, 255, 255);
Circle c2 = new Circle(50, 100, 5);
c2.fillColor(0.5f, 0.5f, 1.0f);
在这段代码中,对于c1
对象,对fillColor()
的调用有整数参数 0、255 和 255。因此,编译器将这个调用解析为方法fillColor(int red, int green, int blue)
。对于c2
对象,对fillColor()
的调用有参数 0.5f、0.5f 和 1.0f 因此,它将调用解析到fillColor(float hue, float saturation, float brightness)
。
在上面的例子中,方法fillColor()
是一个重载的方法。该方法具有相同的名称和相同数量的参数,但参数的类型不同。也可以用不同数量的参数重载方法。
这种重载方法有助于避免在不同的函数中重复相同的代码。让我们看看清单 2-4 中的一个简单例子。
Listing 2-4. HappyBirthday.java
class HappyBirthday {
// overloaded wish method with String as an argument
public static void wish(String name) {
System.out.println("Happy birthday " + name + "!");
}
// overloaded wish method with no arguments;
// this method in turn invokes wish(String) method
public static void wish() {
wish("to you");
}
public static void main(String []args) {
wish();
wish("dear James Gosling");
}
}
它打印:
Happy birthday to you!
Happy birthday dear James Gosling!
这里,方法wish(String name)
的意思是当知道某人的名字时,祝他“生日快乐”。默认方法wish()
是祝任何人“生日快乐”。可以看到,wish()
方法中不用再写System.out.println
;您可以通过将默认值“to you”作为参数传递给wish()
来重用wish(String)
方法定义。这种重用对于大型和相关的方法定义是有效的,因为它节省了编写和测试相同代码的时间。
构造函数重载
默认构造函数对于创建具有默认初始化值的对象很有用。当您希望在不同的实例化中用不同的值初始化对象时,可以将它们作为参数传递给构造函数。是的,一个类中可以有多个构造函数,这就是构造函数重载。在一个类中,默认构造函数可以用默认初始值初始化对象,而另一个构造函数可以接受需要用于对象实例化的参数。
这里有一个重载构造函数的Circle
类的例子(见清单 2-5 )。
Listing 2-5. Circle.java
public class Circle {
private int xPos;
private int yPos;
private int radius;
// three overloaded constructors for Circle
public Circle(int x, int y, int r) {
xPos = x;
yPos = y;
radius = r;
}
public Circle(int x, int y) {
xPos = x;
yPos = y;
radius = 10; // default radius
}
public Circle() {
xPos = 20; // assume some default values for xPos and yPos
yPos = 20;
radius = 10; // default radius
}
public String toString() {
return "center = (" + xPos + "," + yPos + ") and radius = " + radius;
}
public static void main(String[]s) {
System.out.println(new Circle());
System.out.println(new Circle(50, 100));
System.out.println(new Circle(25, 50, 5));
}
}
这个程序打印
center = (20,20) and radius = 10
center = (50,100) and radius = 10
center = (25,50) and radius = 5
正如您所看到的,编译器已经根据参数的数量解析了构造函数调用。默认的构造函数没有参数,在这种情况下,我们为xPos
、yPos
和radius
假设了一些默认值(分别为值 20、20 和 10)。带有两个参数(int x
和 int y
)的Circle
构造函数根据传递的参数值设置xPos
和yPos
的位置,并假设 radius 成员的默认值为 10。接受所有三个参数的Circle
构造函数在Circle
类中设置相应的字段。
您是否注意到您在这三个构造函数中复制了代码?为了避免代码重复,并减少您的输入工作,您可以从一个构造函数调用另一个构造函数。在这三个构造函数中,采用 x 位置、y 位置和半径的构造函数是最通用的构造函数。其他两个构造函数可以通过调用三个参数构造函数来重写,如下所示:
public Circle(int x, int y, int r) {
xPos = x;
yPos = y;
radius = r;
}
public Circle(int x, int y) {
this(x, y, 10); // passing default radius 10
}
public Circle() {
this(20, 20, 10);
// assume some default values for xPos, yPos and radius
}
输出与前一个程序完全相同,但是这个程序更短。在这种情况下,您使用了this
关键字(指当前对象)从同一个类的另一个构造函数中调用一个构造函数。
霸王决议
定义重载方法时,编译器如何知道调用哪个方法?你能猜出清单 2-6 中代码的输出吗?
Listing 2-6. Overloaded.java
class Overloaded {
public static void aMethod (int val) { System.out.println ("int"); }
public static void aMethod (short val) { System.out.println ("short"); }
public static void aMethod (Object val) { System.out.println ("object"); }
public static void aMethod (String val) { System.out.println ("String"); }
public static void main(String[] args) {
byte b = 9;
aMethod(b); // first call
aMethod(9); // second call
Integer i = 9;
aMethod(i); // third call
aMethod("9"); // fourth call
}
}
它可以打印
short
int
object
String
下面是编译器如何解析这些调用:
In the first method call, the statement is aMethod(b)
where the variable b
is of type byte
. There is no aMethod
definition that takes byte
as an argument. The closest type (in size) is short
type and not int
, so the compiler resolves the call aMethod(b)
to aMethod(short val)
definition. In the second method call, the statement is aMethod(9)
. The constant value 9 is of type int
. The closest match is aMethod(int)
, so the compiler resolves the call aMethod(9)
to aMethod(int val)
definition. The third method call is aMethod(i)
, where the variable i
is of type Integer
. There is no aMethod
definition that takes Integer
as an argument. The closest match is aMethod(Object val)
, so it is called. Why not aMethod(int val)
? For finding the closest match, the compiler allows implicit upcasts, not downcasts, so aMethod(int val)
is not considered. The last method call is aMethod("9")
. The argument is a String
type. Since there is an exact match, aMethod(String val)
is called.
编译器试图从给定的重载方法定义中解析方法调用的过程称为重载解析。为了解析方法调用,它首先寻找完全匹配的方法——参数数量和参数类型完全相同的方法定义。如果找不到精确匹配,它会使用向上转换来寻找最接近的匹配。如果编译器找不到任何匹配,那么您将得到一个编译器错误,如清单 2-7 所示。
Listing 2-7. OverloadingError.java
class OverloadingError {
public static void aMethod (byte val ) { System.out.println ("byte"); }
public static void aMethod (short val ) { System.out.println ("short"); }
public static void main(String[] args) {
aMethod(9);
}
}
以下是编译器错误:
OverloadingError.java:6: error: no suitable method found for aMethod(int)
aMethod(9);
^
method OverloadingError.aMethod(byte) is not applicable
(argument mismatch; possible lossy conversion from int to byte)
method OverloadingError.aMethod(short) is not applicable
(argument mismatch; possible lossy conversion from int to short)
1 error
常量 9 的类型是int
,所以对于调用aMethod(9)
,没有匹配的aMethod
定义。正如您之前看到的重载决策,编译器可以对最接近的匹配进行向上转换(例如从byte
到int
),但是它不考虑向下转换(例如从int
到byte
或者从int
到short
,就像本例中一样)。因此,编译器找不到任何匹配,并向您抛出一个错误。
如果编译器找到两个匹配怎么办?也会变成错误!清单 2-8 显示了一个例子。
Listing 2-8. AmbiguousOverload.java
class AmbiguousOverload {
public static void aMethod (long val1, int val2) {
System.out.println ("long, int");
}
public static void aMethod (int val1, long val2) {
System.out.println ("int, long");
}
public static void main(String[] args) {
aMethod(9, 10);
}
}
以下是编译器错误:
AmbiguousOverload.java:11: error: reference to aMethod is ambiguous
aMethod(9, 10);
^
both method aMethod(long,int) in AmbiguousOverload and method aMethod(int,long) in AmbiguousOverload match
1 error
为什么这个电话变成了“暧昧”电话?常数 9 和 10 是int
s,aMethod
有两种定义:一种是aMethod(long, int)
,另一种是aMethod(int, long
。所以没有完全匹配的电话aMethod(int, int)
。整数可以隐式上推至long
和Integer
。编译器会选择哪一个?因为有两个匹配,编译器报错说调用不明确。
如果没有匹配或不明确的匹配,重载决策将失败(并出现编译器错误)。
要记住的要点
这里有一些关于方法重载的有趣规则,对你参加 OCPJP 八级考试有帮助:
- 重载决策完全发生在编译时(而不是运行时)。
- 不能仅用返回类型不同的方法重载方法。
- 不能仅用异常规范不同的方法重载方法。
- 要使重载决策成功,您需要定义方法,以便编译器找到一个精确匹配。如果编译器没有为您的调用找到匹配项,或者匹配项不明确,重载决策将失败,编译器将发出一个错误。
方法的签名由方法名、参数数量和参数类型组成。您可以重载名称相同但签名不同的方法。由于返回类型和异常规范不是签名的一部分,因此不能仅基于返回类型或异常规范重载方法。
覆盖对象类中的方法
认证目标 |
---|
重写 Object 类的 hashCode、equals 和 toString 方法 |
现在让我们讨论覆盖Object
类中的一些方法。您可以在您的类中覆盖clone()
、equals()
、hashCode()
、toString()
和finalize()
方法。因为getClass(), notify()
、notifyAll()
和wait()
方法的重载版本被声明为final
,所以不能覆盖这些方法。
为什么我们要覆盖Object
类中的方法?为了回答这个问题,让我们讨论一下当我们不重写toString()
方法时会发生什么(列出 2-9 )。
Listing 2-9. Point.java
class Point {
private int xPos, yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
public static void main(String []args) {
// Passing a Point object to println
// automatically invokes the toString method
System.out.println(new Point(10, 20));
}
}
它可以打印
Point@19821f (Actual address might differ on your machine, but a similar string will show up)
toString()
方法是在Object
类中定义的,它被 Java 中的所有类继承。下面是在Object
类中定义的toString()
方法的概述:
public String toString()
toString()
方法不带参数,返回对象的String
表示。这个方法的默认实现返回对象 hashcode 的ClassName@hex
版本。这就是为什么您会得到这个不可读的输出。注意,这个十六进制值对于每个实例都是不同的,所以如果您尝试这个程序,您将得到一个不同的十六进制值作为输出。例如,当我们再次运行这个程序时,我们得到了这个输出:Point@affc70
。因此,我们需要在这个Point
类中覆盖toString
方法。
覆盖 toString()方法
当您创建新类时,您应该重写此方法以返回您的类的所需文本表示。清单 2-10 显示了一个改进版本的Point
类,其中覆盖了版本的toString()
方法。
Listing 2-10. Point.java
// improved version of the Point class with overridden toString method
class Point {
private int xPos, yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
// this toString method overrides the default toString method implementation
// provided in the Object base class
public String toString() {
return "x = " + xPos + ", y = " + yPos;
}
public static void main(String []args) {
System.out.println(new Point(10, 20));
}
}
这个程序现在打印
x = 10, y = 20
如你所料,这要干净得多。为了清楚起见,下面是这个Point
类实现中的main()
方法的一个稍微不同的版本:
public static void main(String []args) {
Object obj = new Point(10, 20);
System.out.println(obj);
}
它可以打印
x = 10, y = 20
这里,obj
变量的静态类型是Object
类,对象的动态类型是Point
。println
语句调用obj
变量的toString()
方法。这里,派生类的方法toString()
—Point
的toString()
方法由于运行时多态性而被调用。
压倒一切的问题
在重写时,您需要注意访问级别、方法名及其签名。下面是刚刚讨论的Point
类中的toString()
方法:
public String toString() {
return "x = " + xPos + ", y = " + yPos;
}
在这个方法定义中使用protected
访问说明符代替public
怎么样?有用吗?
protected String toString() {
return "x = " + xPos + ", y = " + yPos;
}
不,不是的。对于这种变化,编译器会报错
Point.java:12: error: toString() in Point cannot override toString() in Object
protected String toString() {
^
attempting to assign weaker access privileges; was public
1 error
在重写时,您可以提供更强的访问权限,而不是更弱的访问权限;否则会变成编译器错误。
下面是另一个稍微修改过的toString()
方法。有用吗?
public Object toString() {
return "x = " + xPos + ", y = " + yPos;
}
您会得到以下编译器错误:
Point.java:12: error: toString() in Point cannot override toString() in Object
public Object toString() {
^
return type Object is not compatible with String
1 error
在这种情况下,您会得到一个不匹配的编译器错误,因为重写方法中的返回类型应该与基类方法完全相同。
这是另一个例子:
public String ToString() {
return "x = " + xPos + ", y = " + yPos;
}
现在编译器不抱怨了。但这是一个名为ToString
的新方法,与Object
中的toString
方法无关。因此,这个ToString
方法不会覆盖toString
方法。
请记住以下几点,以便进行正确的覆盖。重写方法
- 应该具有与基本版本相同的参数列表类型(或兼容类型)。
- 应该具有相同的返回类型。
- 但是从 Java 5 开始,返回类型可以是一个子类——协变返回类型(您很快就会了解到)。
- 不应具有比基本版本更严格的访问修饰符。
- 但是它可能具有限制较少的访问修饰符。
- 不应引发新的或更广泛的已检查异常。
- 但是它可能抛出更少或更窄的检查异常,或者任何未检查的异常。
- 哦,是的,名字应该完全匹配!
请记住,如果没有继承方法,就不能重写它。私有方法不能被重写,因为它们不是继承的。
基方法和重写方法的签名应该兼容,以便进行重写。不正确的重写是 Java 程序中常见的错误来源。在与覆盖相关的问题中,回答问题时要注意覆盖中的错误或问题。
Covariant Return Types
您知道在重写方法时,方法的返回类型应该完全匹配。然而,通过 Java 5 中引入的协变返回类型特性,您可以在覆盖方法中提供返回类型的派生类。嗯,那太好了,但是你为什么需要这个特性呢?签出这些具有相同返回类型的重写方法:
abstract class Shape {
// other methods elided
public abstract Shape copy();
}
class Circle extends Shape {
// other methods elided
public Circle(int x, int y, int radius) { /* initialize fields here */ }
public Shape copy() { /* return a copy of this object */ }
}
class Test {
public static void main(String []args) {
Circle c1 = new Circle(10, 20, 30);
Circle c2 = c1.copy();
}
}
这段代码将给出一个编译器错误"incompatible types: Shape cannot be converted to Circle"
。这是因为在赋值"Circle c2 = c1.copy();"
中缺少从Shape
到Circle
的显式向下转换。
因为您清楚地知道您将分配从Circle
的 copy 方法返回的Circle
对象,所以您可以进行显式强制转换来修复编译器错误:
Circle c2 = (Circle) c1.copy();
由于提供这种向下转换很繁琐(或多或少没有意义),Java 提供了协变返回类型,您可以在重写方法中给出返回类型的派生类。换句话说,您可以如下更改Circle
类中copy
方法的定义:
public Circle copy() { /* return a copy of this object */ }
现在 main 方法Circle c2 = c1.copy();
中的赋值是有效的,不需要显式向下转换(这很好)。
重写 equals()方法
现在让我们覆盖Point
类中的equals
方法。在此之前,下面是Object
类中equals()
方法的签名:
public boolean equals(Object obj)
Object
类中的equals()
方法是一个可重写的方法,它将Object
类型作为参数。它检查当前对象的内容和传递的obj
参数是否相等。如果是,则equals()
返回 true 否则返回 false。
现在,让我们增强清单 2-10 中的代码,并覆盖名为Point
的类中的equals()
方法(参见清单 2-11 )。这是正确的实现吗?
Listing 2-11. Point.java
public class Point {
private int xPos, yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
// override the equals method to perform
// "deep" comparison of two Point objects
public boolean equals(Point other){
if(other == null)
return false;
// two points are equal only if their x and y positions are equal
if( (xPos == other.xPos) && (yPos == other.yPos) )
return true;
else
return false;
}
public static void main(String []args) {
Point p1 = new Point(10, 20);
Point p2 = new Point(50, 100);
Point p3 = new Point(10, 20);
System.out.println("p1 equals p2 is " + p1.equals(p2));
System.out.println("p1 equals p3 is " + p1.equals(p3));
}
}
这张照片
p1 equals p2 is false
p1 equals p3 is true
输出如预期,那么这个equals()
实现是否正确?不要。让我们在main()
方法中做如下微小的修改(代码中的修改用下划线突出显示,就像这样):
public static void main(String []args) {
Object
p1 = new Point(10, 20);
Object
p2 = new Point(50, 100);
Object
p3 = new Point(10, 20);
System.out.println("p1 equals p2 is " + p1.equals(p2));
System.out.println("p1 equals p3 is " + p1.equals(p3));
}
现在可以打印了
p1 equals p2 is false
p1 equals p3 is false
为什么呢?两种main()
方法是等价的。然而,这个更新的main()
方法使用Object
类型来声明p1
、p2
和p3
。这三个变量的动态类型是Point
,所以它应该调用被覆盖的equals()
方法。然而,重写是错误的:equals()
方法应该使用Object
作为参数,而不是Point
参数!Point
类中equals()
方法的当前实现隐藏了Object
类的equals()
方法。因此,main()
方法调用基础版本,这是Object
类中Point
的默认实现!
如果基类方法和重写方法的名字或签名不匹配,就会导致微妙的 bug。因此,请确保它们完全相同。
为了克服重载的微妙问题,可以使用 Java 5 中引入的@Override
注释。这个注释向 Java 编译器明确表达了程序员使用方法覆盖的意图。万一编译器对你重写的方法不满意,它会发出抱怨,这对你来说是一个有用的警告。此外,注释使程序更容易理解,因为方法定义前的@Override
注释帮助您理解您正在覆盖一个方法。
下面是带有equals
方法的@Override
注释的代码:
@Override
public boolean equals(Point other) {
if(other == null)
return false;
// two points are equal only if their x and y positions are equal
if((xPos == other.xPos) && (yPos == other.yPos))
return true;
else
return false;
}
您现在会看到这段代码的编译器错误:
Point.java:11: error: method does not override or implement a method from a supertype
@Override
^
1 error
你能如何修理它?您需要将Object
类型传递给equals
方法的参数。清单 2-12 显示了使用固定equals
方法的程序。
Listing 2-12. Point.java
public class Point {
private int xPos, yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
// override the equals method to perform "deep" comparison of two Point objects
@Override
public boolean equals(Object other) {
if(other == null)
return false;
// check if the dynamic type of 'other' is Point
// if 'other' is of any other type than 'Point', the two objects cannot be
// equal if 'other' is of type Point (or one of its derived classes), then
// downcast the object to Point type and then compare members for equality
if(other instanceof Point) {
Point anotherPoint = (Point) other;
// two points are equal only if their x and y positions are equal
if((xPos == anotherPoint.xPos) && (yPos == anotherPoint.yPos))
return true;
}
return false;
}
public static void main(String []args) {
Object p1 = new Point(10, 20);
Object p2 = new Point(50, 100);
Object p3 = new Point(10, 20);
System.out.println("p1 equals p2 is " + p1.equals(p2));
System.out.println("p1 equals p3 is " + p1.equals(p3));
}
}
现在这个程序打印
p1 equals p2 is false
p1 equals p3 is true
这是正确实现equals
方法后的预期输出。
调用超类方法
在被重写的方法中调用基类方法通常很有用。为此,您可以使用super
关键字。在派生类构造函数中,可以使用super
关键字调用基类构造函数。这样的调用应该是构造函数中的第一条语句(如果使用的话)。您也可以使用super
关键字来引用基类成员。在这些情况下,它不必是方法体中的第一条语句。我们来看一个例子。
您实现了一个属于 2D 点的Point
类:它有 x 和 y 位置。您还可以使用 x、y 和 z 位置实现 3D 点类。为此,您不需要从头开始实现它:您可以扩展 2D 点并在 3D 点类中添加 z 位置。首先,您将把Point
类的简单实现重命名为Point2D
。然后您将通过扩展这个Point2D
来创建Point3D
类(参见清单 2-13 和 2-14 )。
Listing 2-13. Point2D.java
class Point2D {
private int xPos, yPos;
public Point2D(int x, int y) {
xPos = x;
yPos = y;
}
public String toString() {
return "x = " + xPos + ", y = " + yPos;
}
public static void main(String []args) {
System.out.println(new Point2D(10, 20));
}
}
Listing 2-14. Point3D.java
// Here is how we can create Point3D class by extending Point2D class
public class Point3D extends Point2D {
private int zPos;
// provide a public constructors that takes three arguments (x, y, and z values)
public Point3D(int x, int y, int z) {
// call the superclass constructor with two arguments
// i.e., call Point2D(int, int) from Point2D(int, int, int) constructor)
super(x, y); // note that super is the first statement in the method
zPos = z;
}
// override toString method as well
public String toString() {
return super.toString() + ", z = " + zPos;
}
// to test if we extended correctly, call the toString method of a Point3D object
public static void main(String []args) {
System.out.println(new Point3D(10, 20, 30));
}
}
这个程序打印
x = 10, y = 20, z = 30
在类Point2D
中,类成员xPos
和yPos
是私有的,所以你不能直接访问它们来在Point3D
构造函数中初始化它们。然而,您可以使用super
关键字调用超类构造函数并传递参数。这里,super(x, y);
调用基类构造函数Point2D(int, int)
。对超类构造函数的这个调用应该是第一条语句;如果你在zPos = z;
之后调用它,你会得到一个编译错误:
public Point3D(int x, int y, int z) {
zPos = z;
super(x, y);
}
Point3D.java:19: call to super must be first statement in constructor
super(x, y);
类似地,您可以使用super
关键字调用派生类Point3D
的toString()
实现中基类Point2D
的toString()
方法。
覆盖 hashCode()方法
正确覆盖equals
和hashCode
方法对于使用HashMap
和HashSet
这样的类很重要,我们将在第四章的中进一步讨论。清单 2-15 是一个简单的Circle
类示例,因此您可以理解在使用HashSets
这样的集合时会出现什么问题。
Listing 2-15. TestCircle.java
// This program shows the importance of overriding equals() and hashCode() methods
import java.util.*;
class Circle {
private int xPos, yPos, radius;
public Circle(int x, int y, int r) {
xPos = x;
yPos = y;
radius = r;
}
public boolean equals(Object arg) {
if(arg == null) return false;
if(this == arg) return true;
if(arg instanceof Circle) {
Circle that = (Circle) arg;
if( (this.xPos == that.xPos) && (this.yPos == that.yPos)
&& (this.radius == that.radius )) {
return true;
}
}
return false;
}
}
class TestCircle {
public static void main(String []args) {
Set<Circle> circleList = new HashSet<Circle>();
circleList.add(new Circle(10, 20, 5));
System.out.println(circleList.contains(new Circle(10, 20, 5)));
}
}
它打印的是false
(不是true
)!为什么呢?Circle
类覆盖了equals()
方法,但是它没有覆盖hashCode()
方法。当你在标准容器中使用Circle
的对象时,就成问题了。为了快速查找,容器比较对象的 hashcode。如果没有覆盖hashCode()
方法,那么——即使传递了具有相同内容的对象——容器也不会找到该对象!所以您需要覆盖hashCode()
方法。
如果你在像
HashSet
或HashMap
这样的容器中使用一个对象,确保你正确地覆盖了hashCode()
和equals()
方法。如果你不这样做,在使用这些容器时,你会得到令人讨厌的惊喜(错误)!
好的,如何覆盖hashCode()
方法?在理想情况下,hashCode()
方法应该为不同的对象返回唯一的散列码。
如果equals()
方法返回 true,那么hashCode()
方法应该返回相同的哈希值。如果对象是不同的(因此equals()
方法返回 false)怎么办?如果对象不同,hashCode()
最好返回不同的值(尽管不是必需的)。原因是很难编写一个hashCode()
方法来为每个不同的对象赋予唯一的值。
方法
hashCode()
和equals()
需要对一个类保持一致。出于实用目的,请确保您遵循这条规则:如果equals()
方法为两个对象返回 true,那么hashCode()
方法应该为它们返回相同的哈希值。
当实现hashCode()
方法时,可以使用类的实例成员的值来创建一个哈希值。下面是Circle
类的hashCode()
方法的一个简单实现:
public int hashCode() {
// use bit-manipulation operators such as ^ to generate close to unique
// hash codes here we are using the magic numbers 7, 11 and 53,
// but you can use any numbers, preferably primes
return (7 * xPos) ^ (11 * yPos) ^ (53 * yPos);
}
现在,如果您运行main()
方法,它将打印“true”。在这个hashCode()
方法的实现中,您将这些值乘以一个质数,并进行逐位运算。如果您想要一个更好的散列函数,您可以为hashCode()
编写复杂的代码,但是这种实现对于实际目的来说已经足够了。
您可以对int
值使用位运算符。其他类型呢,比如浮点值或引用类型?举个例子,这里是java.awt.Point2D
的hashCode()
实现,有浮点值x
和y
。方法getX()
和getY()
分别返回x
和y
值:
public int hashCode() {
long bits = java.lang.Double.doubleToLongBits(getX());
bits ^= java.lang.Double.doubleToLongBits(getY()) * 31;
return (((int) bits) ^ ((int) (bits >> 32)));
}
这个方法使用了doubleToLongBits()
方法,它接受一个double
值并返回一个long
值。对于浮点值x
和y
(由getX
和getY
方法返回),您以位的形式获得long
值,并使用位操作来获得hashCode()
。
现在,如果类有引用类型成员,如何实现hashCode
方法?例如,考虑使用Point
类的实例作为成员,而不是使用xPos
和yPos
,它们是基本类型字段:
class Circle {
private int radius;
private Point center;
// other members elided
}
在这种情况下,您可以使用Point
的hashCode()
方法来实现Circle
的hashCode
方法:
public int hashCode() {
return center.hashCode() ^ radius;
}
对象组成
认证目标 |
---|
实现继承,包括可见性修饰符和组合 |
单个抽象提供了某些功能,这些功能需要与其他对象相结合来表示一个更大的抽象:一个由其他更小的对象组成的复合对象。你需要制作这样的复合对象来解决现实生活中的编程问题。在这种情况下,复合对象与包含对象共享 HAS-A 关系,并且底层概念被称为对象组合。
打个比方,计算机是一个包含 CPU、内存和硬盘等其他对象的复合对象。换句话说,计算机对象与其他对象共享一个散列关系。清单 2-16 定义了一个Circle
类,它使用一个Point
对象来定义Circle
的中心。
Listing 2-16. Circle.java
// Point is an independent class and here we are using it with Circle class
class Point {
private int xPos;
private int yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
public String toString() {
return "(" + xPos + "," + yPos + ")";
}
}
// Circle.java
public class Circle {
private Point center; // Circle "contains" a Point object
private int radius;
public Circle(int x, int y, int r) {
center = new Point(x, y);
radius = r;
}
public String toString() {
return "center = " + center + " and radius = " + radius;
}
public static void main(String []s) {
System.out.println(new Circle(10, 10, 20));
}
// other members (constructors, area method, etc) are elided …
}
在这个例子中,Circle
有一个Point
对象。换句话说,Circle
和Point
共享一个 has-a 关系;换句话说,Circle
是一个包含Point
对象的复合对象。这是比拥有独立的整数成员xPos
和yPos
更好的解决方案。为什么?您可以重用由Point
类提供的功能。注意Circle
类中的toString()
方法:
public String toString() {
return "center = " + center + " and radius = " + radius;
}
这里,变量center
的使用扩展到了center.toString()
,因此Point
的toString
方法可以在Circle
的toString
方法中重用。
构成与继承
现在你已经具备了合成和继承的知识(我们在本章前面已经讨论过了)。在某些情况下,很难在两者之间做出选择。重要的是要记住,没有什么是银弹——你不能用一个构造解决所有问题。您需要仔细分析每种情况,并决定哪种结构最适合它。
一个经验法则是分别使用 HAS-A 和 IS-A 短语进行组合和继承。例如,
- 计算机有一个中央处理器。
- 圆形是一种形状。
- 一个圆有一个点。
- 笔记本电脑是一台电脑。
- 向量是一个列表。
这条规则对于识别错误的关系很有用。例如,car 的关系是——轮胎是完全错误的,这意味着在类Car
和Tire
之间不能有继承关系。然而,汽车有一个轮胎(意思是汽车有一个或多个轮胎)的关系是正确的——你可以组成一个包含Tire
对象的Car
对象。
在真实场景中,关系的区别可能并不明显。您了解到可以创建一个基类,并将许多类的通用功能放入其中。然而,许多人忽略了悬挂在这种实践上的一个大警告标志——总是检查在派生类和基类之间是否存在 IS-A 关系。如果 IS-A 关系不成立,最好使用复合而不是继承。
例如,取一组需要共同功能的类DynamicDataSet
和SnapShotDataSet
——比如说,排序。现在,人们可以从排序实现中派生出这些数据集类,如清单 2-17 所示。
Listing 2-17. Sorting.java
import java.awt.List;
public class Sorting {
public List sort(List list) {
// sort implementation
return list;
}
}
class DynamicDataSet extends Sorting {
// DynamicDataSet implementation
}
class SnapshotDataSet extends Sorting {
// SnapshotDataSet implementation
}
你认为这是一个好的解决办法吗?不,这不是一个好的解决方案,原因如下:
- 经验法则在这里不适用。
DynamicDataSet
不是Sorting
类型。如果您在类设计中犯了这样的错误,代价可能会非常高——如果积累了大量错误使用继承关系的代码,以后您可能无法修复它们。比如Stack
在 Java 库中扩展了Vector
。然而堆栈显然不是向量,所以它不仅会产生理解问题,还会导致错误。当您创建 Java 库提供的Stack
类的对象时,您可以在容器中的任何位置添加或删除项目,因为基类是Vector
,它允许您在 vector 中的任何位置删除。 - 如果这两类数据集类都有一个真正的基类,
DataSet
会怎么样?在这种情况下,要么Sorting
将成为DataSet
的基类,要么可以将类Sorting
放在DataSet
和两种类型的数据集之间。这两种解决方案都是错误的。 - 还有另一个具有挑战性的问题:如果一个
DataSet
类想要使用一种排序算法(比如 MergeSort ),而另一个数据集类想要使用不同的排序算法(比如 QuickSort ),该怎么办?你会继承实现两种不同排序算法的两个类吗?首先,不能直接从多个类继承,因为 Java 不支持多类继承。其次,即使你能够以某种方式从两个不同的排序类继承(MergeSort
扩展QuickSort
,QuickSort
扩展DataSet
),那也是一个更糟糕的设计。
在这种情况下,最好使用组合——换句话说,使用 HAS-A 关系而不是 IS-A 关系。清单 2-18 中给出了结果代码。
Listing 2-18. Sorting.java
import java.awt.List;
interface Sorting {
List sort(List list);
}
class MergeSort implements Sorting {
public List sort(List list) {
// sort implementation
return list;
}
}
class QuickSort implements Sorting {
public List sort(List list) {
// sort implementation
return list;
}
}
class DynamicDataSet {
Sorting sorting;
public DynamicDataSet() {
sorting = new MergeSort();
}
// DynamicDataSet implementation
}
class SnapshotDataSet {
Sorting sorting;
public SnapshotDataSet() {
sorting = new QuickSort();
}
// SnapshotDataSet implementation
}
当子类指定基类时使用继承,这样你就可以利用动态多态性。在其他情况下,使用组合来获得易于更改和松散耦合的代码。总的来说,喜欢组合胜过继承。
单例类和不可变类
认证目标 |
---|
创建和使用单例类和不可变类 |
在许多情况下,您需要创建特殊类型的类。在这一节中,让我们讨论两种特殊的类:单例类和不可变类。
创建单例类
有些情况下,您希望确保某个特定类只有一个实例。例如,假设您定义了一个修改注册表的类,或者实现了一个管理打印机假脱机的类,或者实现了一个线程池管理器类。在所有这些情况下,您可能希望通过实例化这些类的不超过一个对象来避免难以发现的错误。在这些情况下,你可以创建一个单例类。
单例类确保只创建该类的一个实例。为了确保访问点,该类控制其对象的实例化。在 Java 开发工具包(JDK)的很多地方都可以找到 Singleton 类,比如java.lang.Runtime
。
图 2-2 显示了一个单例类的类图。它由一个类组成,这个类是你想作为单例创建的。它有一个私有构造函数和一个静态方法来获取 singleton 对象。
图 2-2。
UML class diagram of a singleton class
singleton 类提供了两件事:一个且只有一个类实例,以及一个全局单点访问该对象。
假设您想要实现一个记录应用详细信息的类,以便为调试跟踪应用的执行。为了这个目的,你可能想要确保在你的应用中只存在一个Logger
类的实例,因此你可以使Logger
类成为一个单例类(参见清单 2-19 )。
Listing 2-19. Logger.java
// Logger class must be instantiated only once in the application; it is to ensure that the
// whole of the application makes use of that same logger instance
public class Logger {
// declare the constructor private to prevent clients
// from instantiating an object of this class directly
private Logger() { }
// by default, this field is initialized to null
// the static method to be used by clients to get the instance of the Logger class
private static Logger myInstance;
public static Logger getInstance() {
if(myInstance == null) {
// this is the first time this method is called,
// and that's why myInstance is null
myInstance = new Logger();
}
// return the same object reference any time and
// every time getInstance is called
return myInstance;
}
public void log(String s) {
// a trivial implementation of log where
// we pass the string to be logged to console
System.err.println(s);
}
}
看看Logger
类的单例实现。该类的构造函数被声明为私有的,所以不能简单地使用new
操作符创建一个Logger
类的新实例。获得该类的实例的唯一方法是通过getInstance()
方法调用该类的静态成员方法。这个方法检查一个Logger
对象是否已经存在。如果没有,它创建一个Logger
实例,并将其赋给静态成员变量。这样,无论何时调用getInstance()
方法,它总是会返回Logger
类的同一个对象。
确保您的单例确实是单例
确保你的单例实现只允许类的实例是非常重要的(也是困难的)。例如,清单 2-19 中提供的实现只有在你的应用是单线程的情况下才有效。在多线程的情况下,试图获得一个单例对象可能导致创建多个对象,这当然违背了实现单例的目的。清单 2-20 展示了在多线程环境中实现单例设计模式的Logger
类的一个版本。
Listing 2-20. Logger.java
public class Logger {
private Logger() {
// private constructor to prevent direct instantiation
}
private static Logger myInstance;
public static synchronized Logger getInstance() {
if(myInstance == null)
myInstance = new Logger();
return myInstance;
}
public void log(String s){
// log implementation
System.err.println(s);
}
}
注意这个实现中关键字synchronized
的使用。这个关键字是一种 Java 并发机制,一次只允许一个线程进入同步范围。在关于并发的第十一章中,你会学到更多关于这个关键词的知识。
因此,您同步了整个方法,以便每次只有一个线程可以访问它。这使它成为一个正确的解决方案,但有一个问题:性能差。您希望仅在第一次调用该方法时使该方法同步,但是由于您将整个方法声明为同步的,因此对该方法的所有后续调用都会使其成为性能瓶颈。
清单 2-21 显示了Logger
类的另一个实现,它基于“按需初始化持有者”习惯用法。这个习惯用法使用内部类,不使用任何同步结构(我们在第三章的中讨论内部类)。它利用了内部类在被引用之前不会被加载的事实。
Listing 2-21. Logger.java
public class Logger {
private Logger() {
// private constructor
}
public static class LoggerHolder {
public static Logger logger = new Logger();
}
public static Logger getInstance() {
return LoggerHolder.logger;
}
public void log(String s) {
// log implementation
System.err.println(s);
}
}
对于单线程来说,这是一个有效的解决方案,对于多线程应用也同样适用。然而,在我们结束关于单身族的讨论之前,有两句话要提醒我们。首先,在适当的时候使用单件,但是不要过度使用。第二,确保你的单例实现确保只创建一个实例,即使你的代码是多线程的。
不可变类
什么是不可变对象?一旦对象被创建和初始化,就不能修改。我们可以调用访问器方法(即 getter 方法),复制对象,或者传递对象——但是任何方法都不应该允许修改对象的状态。包装类(如Integer
和Float
)和String
类是不可变类的众所周知的例子。
现在让我们讨论一下String
类。String
是不可变的:一旦你创建了一个String
对象,你就不能修改它。像trim
这样移除前导和尾随空白字符的方法怎么样——这样的方法会修改String
对象的状态吗?不。如果有任何前导或尾随空白字符,trim
方法会删除它们并返回一个新的String
对象,而不是修改那个String
对象。
创建不可变对象有很多好处。让我们在String
类的背景下讨论其中的一些优势:
- 不可变对象比可变对象使用起来更安全。一旦检查了它的值,就可以确保它保持不变,并且不会在背后被修改(被其他代码修改)。因此,当我们使用不可变对象时,就不容易出错。例如,如果您有一个对字符串的引用,并发现它具有字符“contents”,如果您保留该引用并在以后使用它,您可以确保它仍然具有字符“contents”(因为没有代码可以修改它)。
- 不可变对象是线程安全的。例如,一个线程可以访问一个
String
对象,而不用担心当它访问该对象时其他线程是否会改变它——这不可能发生,因为一个String
对象是不可变的。 - 具有相同状态的不可变对象可以通过内部共享状态来节省空间。例如,当内容相同时,
String
对象共享相同的内容(称为“字符串滞留”)。您可以使用intern()
方法来确定:
String str1 = new String("contents");
String str2 = new String("contents");
System.out.println("str1 == str2 is " + (str1 == str2));
System.out.println("str1.intern() == str2.intern() is "
+ (str1.intern() == str2.intern()));
// this code prints:
str1 == str2 is false
str1.intern() == str2.intern() is true
由于使用不可变对象的好处,Joshua Bloch 在他的书《有效的 Java》中强烈鼓励使用不可变类:“类应该是不可变的,除非有非常好的理由使它们可变……如果一个类不能变得不可变,你仍然应该尽可能地限制它的可变性。”
定义不可变的类
在创建自己的不可变对象时,请记住以下几个方面:
- 使字段成为 final,并在构造函数中初始化它们。对于基本类型,字段值是最终的,在它被初始化后,不可能改变状态。对于引用类型,不能更改引用。
- 对于可变的引用类型,您需要考虑更多的方面来确保不变性。为什么呢?即使您将可变引用类型设为 final,成员也可能引用在类外部创建的对象,或者被其他人引用。在这种情况下,
- 确保这些方法不会改变那些可变对象内部的内容。
- 不要共享类外的引用——例如,作为该类中方法的返回值。如果对可变字段的引用可以从类外的代码中访问,它们可能会修改对象的内容。
- 如果必须返回引用,则返回对象的深层副本(这样,即使返回的对象内部的内容发生了变化,原始内容也保持不变)。
- 只提供访问器方法(即 getter 方法),但不提供赋值器方法(即 setter 方法)
- 如果必须对对象的内容进行更改,则创建一个新的不可变对象,并进行必要的更改,然后返回该引用。
- 宣布课程结束。为什么?如果该类是可继承的,则其派生类中的方法可以重写它们并修改字段。
因为final
关键字是在“高级类设计”题目下作为考试题目提到的,所以我们在下一章(第三章)中涉及;如果您不熟悉使用final
关键字,请查看该部分。
现在让我们回顾一下String
类,以理解在它的实现中是如何处理这些方面的:
- 它的所有字段都是私有的。
String
构造函数初始化字段。 - 有
trim
、concat,
、substring
等方法需要改变String
对象的内容。为了确保不变性,这些方法返回新的String
对象和修改后的内容。 String
类是 final,所以不能扩展它和覆盖它的方法。
这里有一个不可变的 circle 类。为了简单起见,这个例子只展示了相关的方法来说明如何定义一个不可变的类(清单 2-22 )。
Listing 2-22. ImmutableCircle.java
// Point is a mutable class
class Point {
private int xPos, yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
public String toString() {
return "x = " + xPos + ", y = " + yPos;
}
int getX() { return xPos; }
int getY() { return yPos; }
}
// ImmutableCircle is an immutable class – the state of its objects
// cannot be modified once the object is created
public final class ImmutableCircle {
private final Point center;
private final int radius;
public ImmutableCircle(int x, int y, int r) {
center = new Point(x, y);
radius = r;
}
public String toString() {
return "center: " + center + " and radius = " + radius;
}
public int getRadius() {
return radius;
}
public Point getCenter() {
// return a copy of the object to avoid
// the value of center changed from code outside the class
return new Point(center.getX(), center.getY());
}
public static void main(String []s) {
System.out.println(new ImmutableCircle(10, 10, 20));
}
// other members are elided …
}
这个程序打印
center: x = 10, y = 10 and radius = 20
注意ImmutableCircle
类定义中的以下方面:
- 声明该类是为了防止继承和覆盖它的方法
- 该类只有最终数据成员,它们是
private
- 因为
center
是一个可变字段,getter 方法getCenter()
返回一个Point
对象的副本
不可变对象也有某些缺点。为了确保不变性,不可变类中的方法最终可能会创建对象的大量副本。例如,每次在ImmutableCircle
类上调用getCenter()
时,这个方法都会创建一个Point
对象的副本并返回它。出于这个原因,我们可能还需要定义一个可变版本的类,例如,一个可变的Circle
类。
在大多数情况下,String
类是有用的,如果我们在一个循环中调用诸如trim
、concat
或substring
之类的方法,这些方法可能会创建许多(临时)String
对象。幸运的是,Java 提供了不可变的StringBuffer
和StringBuilder
类。它们提供了类似于String
的功能,但是您可以改变对象中的内容。因此,根据上下文,我们可以选择使用String
类或者StringBuffer
或StringBuilder
类中的一个。
使用“静态”关键字
认证目标 |
---|
开发在初始化块、变量、方法和类时使用 static 关键字的代码 |
现在让我们讨论如何在 Java 中以不同的方式使用static
关键字。假设你想写一个简单的类,计算它的类类型的对象的数量。清单 2-23 中的程序能运行吗?
Listing 2-23. Counter.java
// Counter class should count the number of instances created from that class
public class Counter {
private int count; // variable to store the number of objects created
// for every Counter object created, the default constructor will be called;
// so, update the counter value inside the default constructor
public Counter() {
count++;
}
public void printCount() { // method to print the counter value so far
System.out.println("Number of instances created so far is: " + count);
}
public static void main(String []args) {
Counter anInstance = new Counter();
anInstance.printCount();
Counter anotherInstance = new Counter();
anotherInstance.printCount();
}
}
该程序的输出是
Number of instances created so far is: 1
Number of instances created so far is: 1
哎呀!从输出中可以清楚地看到,该类没有跟踪所创建的对象的数量。发生了什么事?
您已经使用了一个实例变量count
来跟踪从该类创建的对象的数量。因为类的每个实例都有值 count,所以它总是打印1
!你需要的是一个可以被所有实例共享的变量。这可以通过声明一个变量static
来实现。静态变量与其类相关联,而不是与其对象或实例相关联;因此它们被称为类变量。当程序开始执行时,静态变量只初始化一次。静态变量与该类的所有实例共享其状态。使用静态变量的类名(而不是实例)来访问静态变量。清单 2-24 显示了Counter
类的正确实现,其中count
变量和printCount
方法都声明为静态。
Listing 2-24. Counter.java
// Counter class should count the number of instances created from that class
public class Counter {
private static int count; // variable to store the number of objects created
// for every Counter object created, the default constructor will be called;
// so, update the counter value inside the default constructor
public Counter() {
count++;
}
public static void printCount() { // method to print the counter value so far
System.out.println("Number of instances created so far is: " + count);
}
public static void main(String []args) {
Counter anInstance = new Counter();
// note we call printCount using the class name
// instead of instance variable name
Counter.printCount();
Counter anotherInstance = new Counter();
Counter.printCount();
}
}
这个程序打印
Number of instances created so far is: 1
Number of instances created so far is: 2
这里,静态变量count
在执行开始时被初始化。在第一次创建对象时,计数增加到 1。类似地,当第二个对象被创建时,count
的值变成了 2。如程序输出所示,两个对象都更新了count
变量的同一个副本。
注意我们如何将对printCount()
的调用改为使用类名Counter
,就像在Counter.printCount()
中一样。编译器将接受前面两次对anInstance.printCount()
和anotherInstance.printCount()
的调用,因为使用类名或实例变量名调用静态方法在语义上没有区别。但是,不建议使用实例变量来调用静态方法。习惯上使用实例变量调用实例方法,使用类名调用静态方法。
静态方法只能访问静态变量,并且只能调用静态方法。相反,实例方法(非静态)可以调用静态方法或访问静态变量。
静态块
除了静态变量和方法,您还可以在您的类定义中定义一个静态块。这个静态块将由 JVM 在将类加载到内存中时执行。例如,在前面的例子中,您可以定义一个静态块来将 count 变量初始化为默认值 1,而不是默认值 0,如清单 2-25 所示。
Listing 2-25. Counter.java
public class Counter {
private static int count;
static {
// code in this static block will be executed when
// the JVM loads the class into memory
count = 1;
}
public Counter() {
count++;
}
public static void printCount() {
System.out.println("Number of instances created so far is: " + count);
}
public static void main(String []args) {
Counter anInstance = new Counter();
Counter.printCount();
Counter anotherInstance = new Counter();
Counter.printCount();
}
}
这个程序打印
Number of instances created so far is: 2
Number of instances created so far is: 3
不要混淆静态块和构造函数。当创建类的实例时,将调用构造函数,而当 JVM 加载相应的类时,将调用静态块。
要记住的要点
- 开始程序主执行的
main()
方法总是被声明为静态的。为什么呢?如果它是一个实例方法,就不可能调用它。您必须启动程序才能创建实例,然后调用方法,对吗? - 您不能重写基类中提供的静态方法。为什么呢?基于实例类型,方法调用通过运行时多态性来解决。因为静态方法与类相关联(而不是与实例相关联),所以您不能重写静态方法,并且静态方法的运行时多态性是不可能的。
- 静态方法不能在其主体中使用
this
关键字。为什么呢?请记住,静态方法是与类相关联的,而不是与实例相关联的。只有实例方法有与之关联的隐式引用;因此,类方法没有与之相关联的this
引用。 - 静态方法不能在其主体中使用
super
关键字。为什么?使用super
关键字从派生类中的重写方法调用基类方法。因为不能覆盖静态方法,所以不能在它的主体中使用super
关键字。 - 因为静态方法不能访问实例变量(非静态变量),所以它们最适合于实用函数。这就是为什么 Java 里有很多实用方法的原因。例如,
java.lang.Math
库中的所有方法都是静态的。 - 与调用实例方法相比,调用静态方法被认为效率稍高。这是因为与实例方法不同,编译器在调用静态方法时不需要传递隐式的
this
对象引用。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
实现封装
- 封装:将数据和对其进行操作的功能组合成一个单元。
- 您不能访问派生类中基类的私有方法。
- 您可以从同一包中的类(就像包 private 或 default)以及派生类中访问受保护的方法。
- 如果方法在同一个包中,也可以用默认的访问修饰符来访问它。
- 您可以从任何其他类访问某个类的公共方法。
实现继承,包括可见性修饰符和组合
- 继承:在相关类之间创建层次关系。继承也称为“是-是”关系。
- 您使用
super
关键字来调用基类方法。 - 继承意味着是-A,组合意味着有-A 关系。
- 重构图轻继承。
实现多态性
- 多态性:根据上下文用不同的含义解释相同的消息(即方法调用)。
- 基于对象的动态类型解析方法调用被称为运行时多态性。
- 重载是静态多态(早期绑定)的一个例子,而重写是动态多态(后期绑定)的一个例子。
- 方法重载:创建名称相同但参数类型和/或数量不同的方法。
- 你可以重载构造函数。可以使用
this
关键字在另一个构造函数中调用同一个类的构造函数。 - 重载决策是当方法的重载定义可用时,编译器寻求解决调用的过程。
- 在重写中,方法的名称、参数的数量、参数的类型和返回类型应该完全匹配。
- 在协变返回类型中,可以在重写方法中提供返回类型的派生类。
重写 Object 类的 hashCode、equals 和 toString 方法
- 您可以在您的类中覆盖
clone()
、equals(), hashCode()
、toString()
和finalize()
方法。因为getClass()
、notify()
、notifyAll()
和wait()
方法的重载版本被声明为final
,所以不能覆盖这些方法。 - 如果你在像
HashSet
或HashMap
这样的容器中使用一个对象,确保你正确地覆盖了hashCode()
和equals()
方法。例如,如果equals()
方法为两个对象返回 true,确保hashCode()
方法为它们返回相同的哈希值。
创建和使用单例类和不可变类
- 单例确保只创建其类的一个对象。
- 确保预期的单例实现确实是单例的是一项不简单的任务,尤其是在多线程环境中。
- 一旦不可变对象被创建和初始化,它就不能被修改。
- 不可变对象比可变对象使用起来更安全;此外,不可变对象是线程安全的;此外,具有相同状态的不可变对象可以通过内部共享状态来节省空间。
- 要定义一个不可变的类,就把它变成 final。使其所有字段成为私有的和最终的。只提供存取方法(即 getter 方法),但不提供变异方法。对于可变引用类型的字段,或者需要改变状态的方法,如果需要,创建对象的深层副本。
开发在初始化块、变量、方法和类时使用 static 关键字的代码
- 有两种类型的成员变量:类变量和实例变量。所有需要类的实例(对象)来访问的变量都称为实例变量。在所有实例之间共享的并且与一个类而不是一个对象相关联的所有变量被称为类变量(使用
static
关键字声明)。 - 所有静态成员都不需要实例来调用/访问它们。您可以使用类名直接调用/访问它们。
- 静态成员只能调用/访问同一类的静态成员。
Question TimeWhat will be the output of this program? class Color {
int red, green, blue;
void Color() {
red = 10;
green = 10;
blue = 10;
}
void printColor() {
System.out.println("red: " + red + " green: " + green + " blue: " + blue);
}
public static void main(String [] args) {
Color color = new Color();
color.printColor();
}
}
Compiler error: no constructor provided for the class Compiles fine, and when run, it prints the following: red: 0 green: 0 blue: 0 Compiles fine, and when run, it prints the following: red: 10 green: 10 blue: 10 Compiles fine, and when run, crashes by throwing NullPointerException
Consider the following program and predict the behavior of this program: class Base {
public void print() {
System.out.println("Base:print");
}
}
abstract class Test extends Base { //#1
public static void main(String[] args) {
Base obj = new Base();
obj.print(); //#2
}
}
Compiler error “an abstract class cannot extend from a concrete class” at statement marked with comment #1 Compiler error “cannot resolve call to print method” at statement marked with comment #2 The program prints the following: Base:print
The program will throw a runtime exception of AbstractClassInstantiationException
Consider the following program: class Base {}
class DeriOne extends Base {}
class DeriTwo extends Base {}
class ArrayStore {
public static void main(String []args) {
Base [] baseArr = new DeriOne[3];
baseArr[0] = new DeriOne();
baseArr[2] = new DeriTwo();
System.out.println(baseArr.length);
}
}
Which one of the following options correctly describes the behavior of this program? This program prints the following: 3 This program prints the following: 2 This program throws an ArrayStoreException
This program throws an ArrayIndexOutOfBoundsException
Determine the output of this program: class Color {
int red, green, blue;
Color() {
Color(10, 10, 10);
}
Color(int r, int g, int b) {
red = r;
green = g;
blue = b;
}
void printColor() {
System.out.println("red: " + red + " green: " + green + " blue: " + blue);
}
public static void main(String [] args) {
Color color = new Color();
color.printColor();
}
}
Compiler error: cannot find symbol Compiles without errors, and when run, it prints: red: 0 green: 0 blue: 0 Compiles without errors, and when run, it prints: red: 10 green: 10 blue: 10 Compiles without errors, and when run, crashes by throwing NullPointerException
Choose the correct option based on this code segment: class Rectangle { }
class ColoredRectangle extends Rectangle { }
class RoundedRectangle extends Rectangle { }
class ColoredRoundedRectangle extends ColoredRectangle, RoundedRectangle { }
Choose an appropriate option: Compiler error: '{' expected cannot extend two classes Compiles fine, and when run, crashes with the exception MultipleClassInheritanceException
Compiler error: class definition cannot be empty Compiles fine, and when run, crashes with the exception EmptyClassDefinitionError
Consider the following program and determine the output: class Test {
public void print(Integer i) {
System.out.println("Integer");
}
public void print(int i) {
System.out.println("int");
}
public void print(long i) {
System.out.println("long");
}
public static void main(String args[]) {
Test test = new Test();
test.print(10);
}
}
The program results in a compiler error (“ambiguous overload”) long
Integer
int
Consider the following code and choose the right option for the word // Shape.java
public class Shape {
protected void display() {
System.out.println("Display-base");
}
}
// Circle.java
public class Circle extends Shape {
<access-modifier> void display(){
System.out.println("Display-derived");
}
}
Only protected can be used Public and protected both can be used Public, protected, and private can be used Only public can be used Which of the following method(s) from Object
class can be overridden? (Select all that apply.) finalize()
method clone()
method getClass()
method notify()
method E.wait()
method Choose the correct option based on the following program: class Color {
int red, green, blue;
Color() {
this(10, 10, 10);
}
Color(int r, int g, int b) {
red = r;
green = g;
blue = b;
}
public String toString() {
return "The color is: " + red + green + blue;
}
public static void main(String [] args) {
System.out.println(new Color());
}
}
Compiler error: incompatible types Compiles fine, and when run, it prints the following: The color is: 30 Compiles fine, and when run, it prints the following: The color is: 101010 Compiles fine, and when run, it prints the following: The color is: red green blue Choose the best option based on the following program: class Color {
int red, green, blue;
Color() {
this(10, 10, 10);
}
Color(int r, int g, int b) {
red = r;
green = g;
blue = b;
}
String toString() {
return "The color is: " + " red = " + red + " green = " + green + " blue = " + blue;
}
public static void main(String [] args) {
// implicitly invoke toString method
System.out.println(new Color());
}
}
Compiler error: attempting to assign weaker access privileges; toString
was public in Object
Compiles fine, and when run, it prints the following: The color is: red = 10 green = 10 blue = 10 Compiles fine, and when run, it prints the following: The color is: red = 0 green = 0 blue = 0 Compiles fine, and when run, it throws ClassCastException
答案:
B. Compiles fine, and when run, it prints the following: red: 0 green: 0 blue: 0 Remember that a constructor does not have a return type; if a return type is provided, it is treated as a method in that class. In this case, since Color
had void return type, it became a method named Color()
in the Color
class, with the default Color
constructor provided by the compiler. By default, data values are initialized to zero, hence the output. C. The program prints the following: Base:print
It is possible for an abstract class to extend a concrete class, though such inheritance often doesn’t make much sense. Also, an abstract class can have static methods. Since you don’t need to create an object of a class to invoke a static method in that class, you can invoke the main()
method defined in an abstract class. C. This program throws an ArrayStoreException
The variable baseArr
is of type Base[]
, and it points to an array of type DeriOne
. However, in the statement baseArr[2] = new DeriTwo()
, an object of type DeriTwo
is assigned to the type DeriOne
, which does not share a parent-child inheritance relationship-they only have a common parent, which is Base
. Hence, this assignment results in an ArrayStoreException
. A. Compiler error: cannot find symbol The compiler looks for the method Color()
when it reaches this statement: Color(10, 10, 10);
. The right way to call another constructor is to use the this
keyword as follows: this(10, 10, 10);
. A. Compiler error: ‘{’ expected – cannot extend two classes Java does not support multiple class inheritance. Since ColoredRectangle
and RoundedRectangle
are classes, it results in a compiler error when ColoredRoundedRectangle
class attempts to extend these two classes. Note that it is acceptable for a class to be empty. D. int If Integer
and long
types are specified, a literal will match to int
. So, the program prints int
. B. Public and protected both can be used You can provide only a less restrictive or same-access modifier when overriding a method. A. finalize()
method and B. clone()
method The methods finalize()
and clone()
can be overridden. The methods getClass()
, notify()
, and wait()
are final
methods and so cannot be overridden. C. Compiles fine, and when run, it prints the following: The color is: 101010 The toString()
implementation has the expression “The color is:” + red + blue + green. Since the first entry is String, the + operation becomes the string concatenation operator with resulting string “The color is: 10”. Following that, again there is a concatenation operator + and so on until finally it prints “The color is: 101010”. A. Compiler error: attempting to assign weaker access privileges; toString
was public in Object
No access modifier is specified for the toString()
method. Object's toString()
method has a public access modifier; you cannot reduce the visibility of the method. Hence, it will result in a compiler error.
三、高级类设计
认证目标 |
---|
开发使用抽象类和方法的代码 |
开发使用 final 关键字的代码 |
创建内部类,包括静态内部类、局部类、嵌套类和匿名内部类 |
使用枚举类型,包括枚举类型中的方法和构造函数 |
开发声明、实现和/或扩展接口的代码,并使用 atOverride 注释 |
创建和使用 Lambda 表达式 |
在前一章中,您学习了 OOP 的基本概念,并使用它们来构建 Java 程序。在这一章中,你将学习高级的类设计概念。您还将了解 Java 8: lambda 表达式中引入的关键函数式编程特性。
OCPJP 考试中很大一部分问题与 Java 语言和 Java 8 函数库的变化有关。本章涵盖了 lambda 表达式,它构成了理解 Stream API 和java.util.function
包中可用设施的基础。因此,请务必阅读本章中关于 lambda 表达式的接口部分和最后一部分。
抽象类
认证目标 |
---|
开发使用抽象类和方法的代码 |
在许多编程情况下,您希望指定一个抽象,而不指定实现级别的细节。在这种情况下,您可以使用抽象类或接口。当您想要定义一个具有一些公共功能的抽象时,可以使用抽象类。
考虑一下Shape
类,它提供了您可以在绘图应用中绘制的不同形状的抽象。
abstract class Shape {
public double area() { return 0; } // default implementation
// other members
}
在类定义前加上关键字abstract
,将该类声明为抽象类。你可以创建Shapes
的对象比如Square
、Circle
,但是直接创建一个Shape
类本身的对象有意义吗?不,没有名为Shape
的真实世界物体。
如果你试图创建一个Shape
类的实例,编译器会给出一个错误,因为抽象类不能被实例化。
在Shape
类定义中,有一个名为area()
的方法返回特定形状的面积。这个方法适用于所有形状,这就是为什么它在这个基类Shape
中。然而,Shape
类中的area()
方法的实现应该是怎样的呢?您不能提供默认实现;将这个方法实现为return 0;
是一个糟糕的解决方案,尽管编译器会很乐意接受它。更好的解决方案是将其声明为抽象方法,如下所示:
public abstract double area(); // note: no implementation (i.e., no method body definition)
与声明类抽象类似,通过在方法前面加上关键字abstract
来声明方法area()
是抽象的。普通方法和抽象方法的主要区别在于,你不需要为抽象方法提供主体。如果你提供一个主体,它将变成一个错误,就像这样:
public abstract double area() { return 0; } // compiler error!
对于这个定义,您会得到一个编译器错误:"abstract methods cannot have a body"
。抽象方法声明迫使所有子类提供该抽象方法的定义,这就是为什么它不能在抽象类本身中定义的原因。如果派生类没有实现基类中定义的所有抽象方法,则应该将该派生类声明为抽象类,如下例所示:
abstract class Shape {
public abstract double area(); // no implementation
// other members
}
class Rectangle extends Shape { }
这个代码片段导致了编译器错误"Rectangle is not abstract and does not override abstract method area() in Shape"
。要解决这个问题,您需要声明派生类abstract
或者在派生类中提供area()
方法的定义。将Rectangle
声明为抽象是没有意义的;所以你可以这样定义area()
方法:
class Rectangle extends Shape {
private int length, height;
public double area() { return length * height; }
// other members …
}
要记住的要点
复习以下关于 OCPJP 八级考试抽象类和抽象方法的要点:
abstract
关键字可以应用于一个类或非静态方法。- 抽象类可能有声明为静态的方法或字段。然而,
abstract
关键字不能应用于字段或静态方法。 - 一个抽象类可以扩展另一个抽象类,也可以实现一个接口。
- 抽象类可以从具体类派生出来!虽然语言允许,但这样做并不是一个好主意。
- 抽象类不需要声明抽象方法,这意味着抽象类不需要将任何方法声明为抽象。但是,如果一个类有一个抽象方法,它应该被声明为一个抽象类。
- 抽象类的子类需要提供所有抽象方法的实现;否则,您需要将该子类声明为抽象类。
使用“最终”关键字
认证目标 |
---|
开发使用 final 关键字的代码 |
final
关键字可以应用于类、方法和变量。不能扩展 final 类,不能重写 final 方法,也不能在 final 变量初始化后更改其值。
最终课程
final 类是一个不可继承的类,也就是说,如果你声明一个类为 final,你就不能继承它。您可能不希望一个类被子类化的两个重要原因是:
To prevent a behavior change by subclassing. In some cases, you may think that the implementation of the class is complete and should not change. If overriding is allowed, then the behavior of methods might be changed. You know that a derived object can be used where a base class object is required, and you may not prefer it in some cases. By making a class final, the users of the class are assured the unchanged behavior. Improved performance. All method calls of a final class can be resolved at compile time itself. As there is no possibility of overriding the methods, it is not necessary to resolve the actual call at runtime for final classes, which translates to improved performance. For the same reason, final classes encourage the inlining of methods. With inlining, a method body can be expanded as part of the calling code itself, thereby avoiding the overhead of making a function call. If the calls are to be resolved at runtime, they cannot be inlined.
在 Java 库中,很多类都声明为final
;例如,String (java.lang.String)
和System (java.lang.System)
类。这些类在 Java 程序中被广泛使用。如果这两个类没有被声明final
,有人可能通过子类化来改变这些类的行为,然后整个程序可以开始不同的行为。为了避免这样的问题,像这样广泛使用的类和包装类如Number
和Integer
都在 Java 库中做成了final
。
使一个类成为 final 类的性能增益是适度的;重点应该是在适当的地方使用
final
。OCPJP 8 考试主要会考查你是否知道如何正确使用final
关键词。你不用担心效率细节。
最终方法和变量
在一个类中,你可以声明一个方法为 final。final 方法不能被重写。因此,如果您已经在非 final 类中将某个方法声明为 final,则可以扩展该类,但不能重写 final 方法。但是,基类中的其他非最终方法可以在派生类实现中重写。
考虑一下Shape
类中的方法setParentShape()
和getParentShape()
(列表 3-1 )。
Listing 3-1. Shape.java
public abstract class Shape {
// other class members elided
final public void setParentShape(Shape shape) {
// method body
}
public Shape getParentShape() {
// method body
}
}
在这种情况下,Circle
类(Shape
的子类)只能覆盖getParentShape()
;如果您试图覆盖 final 方法,您将得到以下错误:"Cannot override the final method from Shape"
。
Final 变量就像光盘:一旦你在上面写了什么,你就不能再写了。在编程中,像 PI 这样的常量可以被声明为 final,因为你不希望任何人修改它们的值。如果你试图在初始化之后改变一个最终变量,你将得到一个编译器错误。
要记住的要点
复习以下要点,因为它们可能会在 OCPJP 八级考试中出现:
final
修饰符可以应用于一个类、方法或变量。final 类的所有方法都是隐式的final
(因此是不可重写的)。- 一个
final
变量只能赋值一次。如果变量声明将变量定义为final
但没有初始化它,那么它被称为 blank final。您需要在类或初始化块中定义的所有构造函数中初始化一个空白的 final。 - 关键字
final
可以应用于参数。一旦分配,final
参数的值就不能更改。
嵌套类的风格
认证目标 |
---|
创建内部类,包括静态内部类、局部类、嵌套类和匿名内部类 |
在另一个类(或接口)体内定义的类称为嵌套类。通常定义一个类,它是直接属于包的顶级类。相反,嵌套类是包含在另一个类或接口中的类。
在另一个类或接口中创建类有什么好处?有几个好处。首先,您可以将相关的类放在一起作为一个逻辑组。其次,嵌套类可以访问封闭类的所有类成员,这在某些情况下可能很有用。第三,嵌套类简化了代码。例如,匿名内部类对于用 AWT/Swing 编写更简单的事件处理代码很有用。
Java 中有四种类型或风格的嵌套类:
- 静态嵌套类
- 内部类
- 局部内部类
- 匿名内部类
乍一看,这四种味道之间的区别并不明显。图 3-1 有助于阐明它们之间的区别。局部类在代码块(无论是方法、构造函数还是初始化块)中定义,而非局部类在类中定义。静态类使用static
关键字限定,而非静态类在类定义中不使用static
关键字。在匿名类中,你不提供类名;你只是定义它的身体。
图 3-1。
Types of nested classes with examples
正如你在图 3-1 中看到的,静态嵌套类是静态和非本地的,而内部类是非静态和非本地的。非静态局部嵌套类是局部内部类,局部匿名嵌套类是匿名内部类。
现在,让我们更详细地讨论这四种味道。
静态嵌套类(或接口)
您可以将一个类(或接口)定义为另一个类(或接口)内部的静态成员。由于外部类型可以是类或接口,内部类型也可以是类或接口,因此有四种组合。以下是这四种类型的示例,以便您可以了解它们的语法:
class Outer { // an outer class has a static nested class
static class Inner {}
}
interface Outer { // an outer interface has a static nested class
static class Inner {}
}
class Outer { // an outer class has a static nested interface
static interface Inner {}
}
interface Outer { // an outer interface has a static nested interface
static interface Inner {}
}
您不必在嵌套接口中显式使用static
关键字,因为它是隐式静态的。现在,让我们看一个创建和使用静态嵌套类的例子。
考虑具有字段m_red
、m_green,
和m_blue
的Color
类(列表 3-2 )。因为所有形状都可以着色,所以可以在一个Shape
类中定义Color
类。
Listing 3-2. TestColor.java
abstract class Shape {
public static class Color {
int m_red, m_green, m_blue;
public Color() {
// call the other overloaded Color constructor by passing default values
this(0, 0, 0);
}
public Color(int red, int green, int blue) {
m_red = red; m_green = green; m_blue = blue;
}
public String toString() {
return " red = " + m_red + " green = " + m_green + " blue = " + m_blue;
}
// other color members elided
}
// other Shape members elided
}
public class TestColor {
public static void main(String []args) {
// since Color is a static nested class,
// we access it using the name of the outer class, as in Shape.Color
// note that we do not (and cannot) instantiate Shape class for using Color class
Shape.Color white = new Shape.Color(255, 255, 255);
System.out.println("White color has values:" + white);
}
}
它可以打印
White color has: red = 255 green = 255 blue = 255
在这段代码中,Shape
类被声明为abstract
。您可以看到Color
类被定义为Shape
类中定义的public static
类。TestColor
类使用语法Shape.Color
来引用这个类。除了这个微小的区别,Color
类看起来与在Shape
类之外定义Color
类没有什么不同。因此,静态嵌套类与定义为外部类的类一样好,只是有一点不同——它是在另一个类中物理定义的!
要记住的要点
以下是静态嵌套类(和接口)的一些值得注意的方面,将对你的 OCPJP 8 考试有所帮助:
- 可达性
(public, protected,
等。)
静态嵌套类是由外部类定义的。 - 静态嵌套类的名称用
OuterClassName.NestedClassName
语法表示。 - 当你在一个接口内定义一个内部嵌套类(或接口)时,嵌套类被隐式声明为
public
和static
。这一点很容易记住:接口中的任何字段都被隐式声明为public
和static
,静态嵌套类也有同样的行为。 - 静态嵌套类可以声明为
abstract
或final
。 - 静态嵌套类可以扩展另一个类,也可以用作基类。
- 静态嵌套类可以有静态成员。(您很快就会看到,该语句不适用于其他类型的嵌套类。)
- 静态嵌套类可以访问外部类的成员(显然只有静态成员)。
- 外部类也可以通过嵌套类的对象访问嵌套类的成员(甚至是
private
成员)。如果不声明嵌套类的实例,外部类就不能直接访问嵌套类元素。
内部类
您可以将一个类(或一个接口)定义为另一个类中的非静态成员。在接口内部声明一个类或者一个接口怎么样?正如你在上面关于静态内部类的第三个要点中看到的,当你在一个接口中定义一个类或者一个接口时,它是隐式的static
。所以,不可能声明一个非静态的内部接口!这就剩下了两种可能性:
class Outer { // an outer class has an inner class
class Inner {}
}
class Outer { // an outer class has an inner interface
interface Inner {}
}
让我们创建一个Point
类来实现一个Circle
的中心。既然您想将每个Circle
与一个中心Point
相关联,那么将Point
作为Circle
(清单 3-3 )的内部类是一个好主意。
Listing 3-3. Circle.java
public class Circle {
// define Point as an inner class within Circle class
class Point {
private int xPos;
private int yPos;
// you can provide constructor for an inner class like this
public Point(int x, int y) {
xPos = x;
yPos = y;
}
// the inner class is like any other class - you can override methods here
public String toString() {
return "(" + xPos + "," + yPos + ")";
}
}
// make use of the inner class for declaring a field
private Point center;
private int radius;
public Circle(int x, int y, int r) {
// note how to make use of the inner class to instantiate it
center = this.new Point(x, y);
radius = r;
}
public String toString() {
return "mid point = " + center + " and radius = " + radius;
}
public static void main(String []s) {
System.out.println(new Circle(10, 10, 20));
}
// other methods such as area are elided
}
在这个实现中,您已经将Point
定义为Circle
的私有成员。注意您是如何实例化内部类的:
center = this.new Point(x, y);
您可能想知道为什么不能使用通常的new
语句:
center = new Point(x, y);
您需要为外部类的对象引用添加前缀,以创建内部类的实例。在这种情况下,它是一个this
引用,所以您在new
操作符前加上了前缀this
。
每个内部类都与外部类的一个实例相关联。换句话说,内部类总是与封闭对象相关联。
外部和内部阶层共享一种特殊的关系,就像朋友或同一家庭的成员。不管访问说明符是什么,成员访问都是有效的,比如private
。然而,还是有细微的区别。您可以在内部类中访问外部类的成员,而无需创建实例;但是外部类却不是这样。为了访问内部类的成员(任何成员,包括私有成员),您需要创建一个内部类的实例。
内部类的一个限制是不能在内部类中声明静态成员,如下所示:
class Outer {
class Inner {
static int i = 10;
}
}
如果您尝试这样做,将会得到以下编译器错误:
Outer.java:3: inner classes cannot have static declarations
static int i = 10;
要记住的要点
以下是一些关于内部类和接口的重要规则,可能会在 OCPJP 8 考试中有用:
- 可达性(
public
、protected
等)。)是由外部类定义的。 - 就像顶级类一样,内部类可以扩展类或实现接口。类似地,其他类可以扩展内部类,其他类或接口可以扩展或实现内部接口。
- 内部类可以声明为
final
或abstract
。 - 内部类可以有内部类,但是你将很难阅读或理解如此复杂的类嵌套。(意思:避开他们!)
局部内部类
局部内部类是在代码块中定义的(比如在方法、构造函数或初始化块中)。与静态嵌套类和内部类不同,局部内部类不是外部类的成员;它们只是定义它们的方法或代码的局部变量。
以下是局部类的一般语法示例:
class SomeClass {
void someFunction() {
class Local { }
}
}
正如您在这段代码中看到的,Local
是在someFunction
中定义的一个类。它在someFunction
之外是不可用的,甚至对SomeClass
的成员也不可用。因为你不能声明一个局部变量static
,你也不能声明一个局部类static
。
因为不能在接口中定义方法,所以在接口中不能有局部类或接口。也不能创建本地接口。换句话说,不能在方法、构造函数和初始化块中定义接口。
现在您已经理解了语法,让我们来看一个实际的例子。之前,您将Color
类实现为静态嵌套类(清单 3-2 )。以下是您在讨论中看到的代码:
abstract class Shape {
public static class Color {
int m_red, m_green, m_blue;
public Color() {
this(0, 0, 0);
}
public Color(int red, int green, int blue) {
m_red = red; m_green = green; m_blue = blue;
}
public String toString() {
return " red = " + m_red + " green = " + m_green + " blue = " + m_blue;
}
// other color members elided
}
// other Shape members elided
}
现在,这个toString()
方法显示了一个Color
的字符串表示。假设您想要以下面的格式显示Color
字符串:"You selected a color with RGB values red = 0 green = 0 blue = 0"
。为此,您必须在类StatusReporter
中定义一个名为getDescriptiveColor()
的方法。在getDescriptiveColor()
中,您必须创建一个Shape.Color
的派生类,其中toString
方法返回这个描述性消息。清单 3-4 是一个使用本地类的实现。
Listing 3-4. StatusReporter.java
class StatusReporter {
// important to note that the argument "color" is declared final
static Shape.Color getDescriptiveColor(final Shape.Color color) {
// local class DescriptiveColor that extends Shape.Color class
class DescriptiveColor extends Shape.Color {
public String toString() {
return "You selected a color with RGB values" + color;
}
}
return new DescriptiveColor();
}
public static void main(String []args) {
Shape.Color descriptiveColor =
StatusReporter.getDescriptiveColor(new Shape.Color(0, 0, 0));
System.out.println(descriptiveColor);
}
}
main 方法检查StatusReporter
是否工作正常。这个程序打印
You selected a color with RGB values red = 0 green = 0 blue = 0
让我们看看局部类是如何定义的。getDescriptiveColor()
方法接受普通的Shape.Color
类对象并返回一个Shape.Color
对象。在getDescriptiveColor()
方法中,您定义了这个方法的本地类DescriptiveColor
。这个DescriptiveColor
是Shape.Color
的派生类。在DescriptiveColor
类中,唯一定义的方法是toString()
方法,它覆盖了基类Shape.Color toString()
方法。在定义了DescriptiveColor
类之后,getDescriptiveColor
类创建一个DescriptiveColor
类的对象并返回它。
在Test
类中,您可以看到一个main()
方法,它只调用了StatusReporter.getDescriptiveColor()
方法并将结果存储在一个Shape.Color
引用中。您会注意到,getDescriptiveColor()
方法返回一个从Shape.Color
派生的DescriptiveColor
对象,因此descriptiveColor
变量初始化工作正常。在println
中,descriptiveColor
的动态类型是DescriptiveColor
对象,因此打印颜色对象的详细描述。
您是否注意到了getDescriptiveColor()
方法的另一个特性?它的参数声明为final
。即使你没有提供 final 关键字,编译器也将把 is 视为有效的 final——这意味着你不能给你在局部类中访问的变量赋值。如果您这样做,将会得到一个编译器错误,如:
static Shape.Color getDescriptiveColor(Shape.Color color) {
// local class DescriptiveColor that extends Shape.Color class
class DescriptiveColor extends Shape.Color {
public String toString() {
return "You selected a color with RGB values" + color;
}
}
color = null; // note this assignment – will NOT compile
return new DescriptiveColor();
}
您将得到以下编译器错误:
StatusReporter.java:8: error: local variables referenced from an inner class must be final or effectively final
return "You selected a color with RGB values" + color;
^
1 error
由于对color
变量的赋值,它不再是最终变量,因此当局部内部类试图访问该变量时,编译器会给出一个错误。
您只能将最终变量传递给局部内部类。如果你没有声明一个局部内部类访问的变量,编译器会把它视为 final 变量。
要记住的要点
以下几点关于地方班的内容可能会在 OCPJP 八级考试中出现:
- 您可以在代码体内创建一个非静态的局部类。接口不能有本地类,您也不能创建本地接口。
- 局部类只能从定义该类的代码体中访问。局部类在定义该类的代码体之外是完全不可访问的。
- 定义局部类时,可以扩展类或实现接口。
- 局部类可以访问定义它的代码体中所有可用的变量。局部内部类访问的变量实际上被认为是最终变量。
匿名内部类
顾名思义,匿名内部类没有名字。该类的声明自动从实例创建表达式中派生。它们也被简称为匿名类。
匿名类在几乎所有可以使用局部内部类的情况下都很有用。局部内部类有名字,而匿名内部类没有——这是主要的区别。另一个区别是匿名内部类不能有任何显式构造函数。构造函数是以类名命名的,因为匿名类没有名字,所以不能定义构造函数!
(在我们继续之前,这里需要注意:没有“匿名接口”这样的东西)
下面是一个理解局部类语法的示例:
class SomeClass {
void someFunction() {
new Object() { };
}
}
这个代码看起来很神秘,不是吗?这是怎么回事?在语句new Object() { };
中,您使用new
关键字直接声明了一个Object
的派生类。它不定义任何代码,而是返回该派生对象的一个实例。创建的对象没有在任何地方使用,所以它被忽略。new
表达式调用这里的默认构造函数;您可以选择通过在new
表达式中传递参数来调用基类的多参数构造函数。
现在让我们看一个更实际的例子。在前面的例子中(清单 3-4 ,您看到了在StatusReporter
类的getDescriptiveColor
方法中定义的DescriptiveColor
类。您可以通过将本地类转换成匿名类来简化代码,如清单 3-5 所示。
Listing 3-5. StatusReporter.java
class StatusReporter {
static Shape.Color getDescriptiveColor(final Shape.Color color) {
// note the use of anonymous inner classes here
// -- specifically, there is no name for the class and we construct
// and use the class "on the fly" in the return statement!
return new Shape.Color() {
public String toString() {
return "You selected a color with RGB values" + color;
}
};
}
public static void main(String []args) {
Shape.Color descriptiveColor =
StatusReporter.getDescriptiveColor(new Shape.Color(0, 0, 0));
System.out.println(descriptiveColor);
}
}
它可以打印
You selected a color with RGB values red = 0 green = 0 blue = 0
真好。程序的其余部分,包括main()
方法,保持不变,而getDescriptiveColor()
方法变得更简单了!你没有明确地创建一个有名字的类(名字是DescriptiveColor
);相反,您只是在 return 语句中“动态地”创建了一个Shape.Color
的派生类。注意,关键字class
也是不需要的。
要记住的要点
请注意以下关于匿名类的要点,它们可能对 OPCJP 8 考试有用:
- 匿名类在
new
表达式本身中定义。 - 定义匿名类时,不能显式扩展类或显式实现接口。
枚举数据类型
认证目标 |
---|
使用枚举类型,包括枚举类型中的方法和构造函数 |
假设您希望用户从定义几种打印机类型的一组常量中进行选择:
public static final int DOTMATRIX = 1;
public static final int INKJET = 2;
public static final int LASER= 3;
解决方案是可行的。然而,在这种情况下,您可以传递任何其他整数(比如 10),编译器会欣然接受。因此,该解决方案不是类型安全的解决方案。在这种情况下,Java 5 引入了数据类型 enum 来帮助您。
清单 3-6 为上面的例子定义了一个枚举类(是的,枚举是特殊的类)。
Listing 3-6. EnumTest.java
// define an enum for classifying printer types
enum PrinterType {
DOTMATRIX, INKJET, LASER
}
// test the enum now
public class EnumTest {
PrinterType printerType;
public EnumTest(PrinterType pType) {
printerType = pType;
}
public void feature() {
// switch based on the printer type passed in the constructor
switch(printerType){
case DOTMATRIX:
System.out.println("Dot-matrix printers are economical and almost obsolete");
break;
case INKJET:
System.out.println("Inkjet printers provide decent quality prints");
break;
case LASER:
System.out.println("Laser printers provide best quality prints");
break;
}
}
public static void main(String[] args) {
EnumTest enumTest = new EnumTest(PrinterType.LASER);
enumTest.feature();
}
}
它可以打印
Laser printers provide best quality prints
让我们更详细地回顾一下清单 3-6 。
- 在 switch-case 语句中,不需要为枚举元素提供完全限定的名称。这是因为 switch 接受枚举类型的实例,因此 switch-case 理解您在其中指定枚举元素的上下文(类型)。
- 在创建枚举对象 E
numTest
时,我们已经提供了值PrinterType.LASER
。如果我们提供除枚举值之外的任何其他值,您将会得到一个编译器错误。换句话说,枚举是类型安全的。
注意,您可以在一个单独的文件中声明一个 enum(在本例中为PrinterType
),就像您可以声明任何其他普通的 Java 类一样。
现在让我们看一个更详细的例子,在这个例子中,您在一个枚举数据类型中定义成员属性和方法(清单 3-7 )。
Listing 3-7. EnumTest.java
enum PrinterType {
DOTMATRIX(5), INKJET(10), LASER(50);
private int pagePrintCapacity;
private PrinterType(int pagePrintCapacity) {
this.pagePrintCapacity = pagePrintCapacity;
}
public int getPrintPageCapacity() {
return pagePrintCapacity;
}
}
public class EnumTest {
PrinterType printerType;
public EnumTest(PrinterType pType) {
printerType = pType;
}
public void feature() {
switch (printerType) {
case DOTMATRIX:
System.out.println("Dot-matrix printers are economical");
break;
case INKJET:
System.out.println("Inkjet printers provide decent quality prints");
break;
case LASER:
System.out.println("Laser printers provide the best quality prints");
break;
}
System.out.println("Print page capacity per minute: " +
printerType.getPrintPageCapacity());
}
public static void main(String[] args) {
EnumTest enumTest1 = new EnumTest(PrinterType.LASER);
enumTest1.feature();
EnumTest enumTest2 = new EnumTest(PrinterType.INKJET);
enumTest2.feature();
}
}
上述程序的输出如下所示:
Laser printers provide the best quality prints
Print page capacity per minute: 50
Inkjet printers provide decent quality prints
Print page capacity per minute: 10
在这个程序中,您为 enum 类定义了一个新属性、一个新构造函数和一个新方法。属性pagePrintCapacity
由 enum 元素(比如LASER(50)
)指定的初始值设置,它调用 enum 类的构造函数。但是,枚举类不能有公共构造函数,否则编译器会报错如下消息:"Illegal modifier for the enum constructor; only private is permitted"
。
枚举类中的构造函数只能指定为私有。
要记住的要点
- 枚举被隐式声明为
public
、static
和final
,这意味着你不能扩展它们。 - 当您定义一个枚举时,它隐式地继承自
java.lang.Enum
。在内部,枚举被转换为类。此外,枚举常数是枚举类的实例,该常数被声明为该枚举类的成员。 - 您可以对 enum 元素应用
valueOf()
和name()
方法来返回 enum 元素的名称。 - 如果你在一个类中声明一个枚举,那么默认情况下它是静态的。
- 不能对枚举数据类型使用 new 运算符,即使在枚举类内部也是如此。
- 可以使用==运算符比较两个枚举是否相等。
- 如果枚举常量来自两个不同的枚举,
equals()
方法不返回 true。 - 当枚举常量的
toString()
方法被调用时,它打印枚举常量的名称。 - 当在枚举类型上被调用时,
Enum
类中的静态values()
方法返回枚举常数的数组。 - 不能克隆枚举常数。试图这样做将导致
CloneNotSupportedException
。
Enum 避免了幻数,提高了源代码的可读性和可理解性。此外,枚举是类型安全的构造。因此,只要需要一组相关的常数,就使用枚举。
接口
认证目标 |
---|
开发声明、实现和/或扩展接口的代码,并使用 atOverride 注释 |
接口是一组定义协议(即行为契约)的抽象方法。实现接口的类必须实现接口中指定的方法。接口定义了一个协议,实现接口的类遵循该协议。换句话说,一个接口通过定义一个抽象向它的客户承诺某些功能。所有实现接口的类都为承诺的功能提供了自己的实现。
声明和实现接口
现在是时候为形状对象实现自己的接口了。一些圆形物体(如Circle
和Ellipse
)可以滚动到给定的程度。您可以创建一个Rollable
接口并声明一个名为roll()
的方法:
interface Rollable {
void roll(float degree);
}
如您所见,您使用interface
关键字定义了一个接口,该关键字声明了一个名为roll()
的方法。该方法采用一个参数:滚动的degree
。现在让我们在一个Circle
类中实现这个接口(参见清单 3-8 )。
Listing 3-8. Circle.java
// Shape is the base class for all shape objects; shape objects that are associated with
// a parent shape object is remembered in the parentShape field
abstract class Shape {
abstract double area();
private Shape parentShape;
public void setParentShape(Shape shape) {
parentShape = shape;
}
public Shape getParentShape() {
return parentShape;
}
}
// Rollable interface can be implemented by circular shapes such as Circle
interface Rollable {
void roll(float degree);
}
abstract class CircularShape extends Shape implements Rollable { }
// Circle is a concrete class that is-a subtype of CircularShape;
// you can roll it and hence implements Rollable through CircularShape base class
public class Circle extends CircularShape {
private int xPos, yPos, radius;
public Circle(int x, int y, int r) {
xPos = x;
yPos = y;
radius = r;
}
public double area() { return Math.PI * radius * radius; }
@Override
public void roll(float degree) {
// implement rolling functionality here...
// for now, just print the rolling degree to console
System.out.printf("rolling circle by %f degrees", degree);
}
public static void main(String[] s) {
Circle circle = new Circle(10,10,20);
circle.roll(45);
}
}
在这种情况下,CircularShape
实现了Rollable
接口并扩展了Shape
抽象类。现在像Circle
这样的具体类可以扩展这个抽象类并定义roll()
方法。本例中需要注意的几个要点是:
- 抽象类
CircularShape
实现了Rollable
接口,但不需要定义roll()
方法。扩展了CircularShape
的具体类Circle
稍后定义了这个方法。 - 您使用关键字
implements
来实现一个接口。注意,类定义中的方法名、参数和返回类型应该与接口中给出的完全匹配;如果它们不匹配,则认为该类没有实现该接口。 - 或者,您可以使用
@Override
注释来指示一个方法正在从它的基类型中重写一个方法。在这种情况下,roll 方法在Circle
类中被覆盖,并使用了@Override
注释。
一个类也可以同时实现多个接口——直接或间接地通过它的基类。例如,Circle
类也可以实现标准的Cloneable
接口(用于创建Circle
对象的副本)和Serializable
接口(用于将对象存储在文件中以便以后重新创建对象,等等。),像这样:
class Circle extends CircularShape implements Cloneable, Serializable {
/* definition of methods such as clone here */
}
要记住的要点
以下是一些关于界面的要点,对你参加 OCPJP 八级考试有所帮助:
- 无法实例化接口。对接口的引用可以引用实现它的任何派生类型的对象。
- 一个接口可以扩展另一个接口。使用
extends
(而不是implements
)关键字来扩展另一个接口。 - 接口不能包含实例变量。如果你在一个接口中声明了一个数据成员,它应该被初始化,所有这样的数据成员都被隐式地当作“
public static final
”成员。 - 一个接口可以有三种方法:抽象方法、默认方法和静态方法。
- 接口可以用空体声明(即没有任何成员的接口)。例如,
java.util
定义了没有主体的接口EventListener
。 - 一个接口可以在另一个接口或类中声明;这种接口被称为嵌套接口。
- 与只能拥有
public
或default
访问权的顶级接口不同,嵌套接口可以声明为public
、protected
或private
。 - 如果在抽象类中实现接口,抽象类不需要定义方法。但是,最终一个具体的类必须定义接口中声明的抽象方法。
- 您可以对一个方法使用
@Override
注释来表明它正在从它的基类型中重写一个方法。
抽象类与接口
抽象类和接口有很多共同点。例如,两者都可以声明所有派生类都应该定义的方法。它们的相似之处还在于,你既不能创建抽象类的实例,也不能创建接口的实例。那么,抽象类和接口有什么区别呢?表 3-1 列出了一些重要的区别。
表 3-1。
Abstract Classes v.s Interfaces
抽象类 | 接口 | |
---|---|---|
使用的关键字 | 使用abstract 和class 关键字定义一个类别。 |
使用interface 关键字定义一个接口。 |
实现类使用的关键字 | 使用extends 关键字从抽象类继承。 |
使用implements 关键字实现一个接口。 |
菲尔茨 | 抽象类可以有静态和非静态字段。 | 接口中不能有非静态字段(实例变量);默认情况下,所有字段都是 public static final(即下一项中讨论的常量) |
常数 | 抽象类可以有静态和非静态常量。 | 接口只能有静态常量。如果声明一个字段,它必须被初始化。所有字段都被隐式地认为是public static 和final 。 |
构造器 | 您可以在抽象类中定义构造函数(例如,这对于初始化字段很有用)。 | 不能在接口中声明/定义构造函数。 |
访问说明符 | 抽象类中可以有私有和受保护的成员。 | 接口中不能有任何私有或受保护的成员;默认情况下,所有成员都是公共的。 |
单一继承与多重继承 | 一个类只能继承一个类(可以是抽象类,也可以是具体类)。 | 一个类可以实现任意数量的接口。 |
目的 | 抽象基类提供协议;此外,它还充当 is-a 关系中的基类。 | 一个接口只提供一个协议。它指定了实现它的类必须提供的功能。 |
抽象、默认和静态方法
您看到的Rollable
示例只有一个方法— roll()
。然而,接口拥有多个方法是很常见的。例如,java.util
将Iterator
接口定义如下:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
该接口用于遍历集合。(不用担心Iterator<E>
里的<E>
。它指的是元素类型,属于泛型,我们将在下一章详细介绍)。它声明了两个方法hasNext()
和next()
——实现这个接口的类必须定义这两个方法。没有必要使用abstract
关键字(但是如果你愿意,你可以提供abstract
关键字),因为没有主体的方法被隐式地认为是抽象的。
该接口还有对remove()
和forEachRemaining()
的方法定义。这些方法被称为默认方法,它们是用default
关键字限定的。实现Iterator
接口的类继承了这两个方法,并且可以选择覆盖它们。
接口也可以包含静态方法。例如,java.util.stream.Stream
有静态方法builder
、empty
、of
、iterate
、generate
和concat
。
在 Java 8 之前,接口只能声明方法(也就是说,它们只能提供抽象方法)。为了支持 lambda 函数,Java 8 对接口做了很大的改变:现在可以在接口中定义默认方法和静态方法。
默认方法
在接口中,默认方法是使用关键字default
用方法体定义的方法。默认方法是实例方法。在默认方法中,this
关键字是指声明接口。默认方法可以从包含它们的接口中调用方法。
Java 8 为什么要给接口添加默认方法?简答:为了支持 lambda 表达式(我们将在下一节讨论 lambdas)。默认方法使得接口的发展变得容易。怎么会?在 Java 8 之前,不能定义方法——只能声明它们。因此,如果您在现有的接口中添加一个新方法,这样的添加会破坏实现该接口的类,因为它们不会定义该方法。但是在 Java 8 中,使用默认方法,可以更容易地进化接口。
以java.lang.Iterable
接口为例。在 Java 8 之前,它只有一种方法:
Iterator<T> iterator();
在 Java 8 中,Iterable
接口又扩展了两个方法:forEach
和spliterator
方法。为了避免破坏实现该接口的类,这些方法被定义为默认方法。所以所有实现了Iterable
接口的类(比如ArrayList
类)现在也有了这两个方法。这里是Iterable
接口的定义,没有文档注释。
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
在这个接口中添加forEach
和spliterator
方法不会破坏实现Iterator
接口的现有类,因为它们是默认方法。这样,默认方法有助于接口的发展。默认方法也简化了您的生活,因为现在可以在接口中提供具体的定义——所以您不需要覆盖它们。
现有库中的许多类(尤其是Collections
)都是用 Java 8 中的默认方法添加的。例如,Java 中的List
接口有这三个在 Java 8 中添加的方法:
default void sort(Comparator<? super E> c)
default Spliterator<E> spliterator()
default void replaceAll(UnaryOperator<E> operator)
要记住的要点
以下是一些关于抽象、默认和静态方法的要点,对你参加 OCPJP 八级考试有帮助:
- 您不能将成员声明为
protected
或private
。只允许接口成员进行public
访问。因为默认情况下所有方法都是公共的,所以可以省略public
关键字。 - 接口中声明的所有方法(即没有方法体)都被隐式地认为是抽象的。如果您愿意,可以为该方法显式使用
abstract
限定符。 - 默认方法必须有方法体。默认方法必须使用关键字
default
进行限定。实现接口的类继承默认的方法定义,并且可以被重写。 - 默认方法可以作为抽象方法在派生类中重写;对于这种覆盖,也可以使用
@Override
注释。 - 您不能将默认方法限定为
synchronized
或final
。 - 静态方法必须有一个方法体,并且使用
static
关键字对它们进行限定。 - 您不能为静态方法提供
abstract
关键字:请记住,您不能在派生类中重写静态方法,所以从概念上讲,通过不提供方法体来使静态方法保持抽象是不可能的。 - 不能对静态方法使用
default
关键字,因为所有默认方法都是实例方法。
钻石问题
在 Java 中,一个接口或类可以扩展多个接口。例如,这里有一个来自java.nio.channels
包的类层次结构(图 3-2 )。基础接口是Channel
。两个接口,ReadableByteChannel
和WriteableByteChannel
,扩展了这个基本接口。最后,ByteChannel
接口扩展了ReadableByteChannel
和WriteableByteChannel.
注意继承层次的结果形状看起来像一个“钻石”
图 3-2。
Diamond hierarchy in java.nio.channels package
在这种情况下,基本接口Channel
没有任何方法。ReadableByteChannel
接口声明了read
方法,WriteableByteChannel
接口声明了write
方法;ByteChannel
接口从这些基本类型中继承了read
和write
方法。由于这两种方法是不同的,我们没有冲突,因此这个层次结构是好的。
但是如果我们在基类型中有两个具有相同签名的方法定义会怎么样呢?ByteChannel
接口会继承哪个方法?当这个问题发生时,它被称为“钻石问题”
在我们讨论处理菱形问题的工作示例之前,让我们首先清楚地了解一下菱形问题在 Java 中是何时以及如何发生的。
- 在 Java 中,你不能扩展多个类;因此,因为扩展了两个基类,所以不会出现菱形问题。然而,当抽象类和接口定义具有相同签名的方法时,在派生类中可能会出现菱形问题。
- 当两个基本接口具有相同签名的抽象方法时,这并不会真正导致“钻石问题”,因为它们是方法声明而不是定义(如 Java 8 之前的情况)。
- 接口只能定义方法而不能定义字段(它们只能包含常量)。因此,对于界面中的字段,不会出现菱形问题;它只出现在方法定义中。
幸运的是,当派生类型从不同的基类型继承同名的方法定义时,可以使用规则来解析方法。让我们在这里讨论两个重要的场景。
场景 1:如果两个超接口用相同的签名定义方法,编译器会发出错误。我们必须手动解决冲突(清单 3-9 )。
Listing 3-9. Diamond.java
interface Interface1 {
default public void foo() { System.out.println("Interface1's foo"); }
}
interface Interface2 {
default public void foo() { System.out.println("Interface2's foo"); }
}
public class Diamond implements Interface1, Interface2 {
public static void main(String []args) {
new Diamond().foo();
}
}
Error:(9, 8) java: class Diamond inherits unrelated defaults for foo() from types Interface1 and Interface2
在这种情况下,通过在Diamond
类中使用 super 关键字来显式地指出要使用哪个方法定义,从而手动解决冲突:
public void foo() { Interface1.super.foo(); }
将该方法定义添加到 Diamond 类中并执行后,该程序将打印:
Interface1's foo
场景 2:如果基类和基接口用相同的签名定义方法,则使用类中的方法定义,忽略接口定义(清单 3-10 )。
Listing 3-10. Diamond.java
class BaseClass {
public void foo() { System.out.println("BaseClass's foo"); }
}
interface BaseInterface {
default public void foo() { System.out.println("BaseInterface's foo"); }
}
public class Diamond extends BaseClass implements BaseInterface {
public static void main(String []args) {
new Diamond().foo();
}
}
这种情况下没有编译器错误:编译器解析类中的定义,接口定义被忽略。这个程序打印“Base foo”。这可以被认为是“阶级胜利”法则。该规则有助于保持与 Java 8 之前版本的兼容性。怎么做?当一个新的默认方法被添加到一个接口中时,它可能碰巧与基类中定义的方法具有相同的签名。通过“类获胜”规则解决冲突,基类中的方法将总是被选择。
功能界面
Java 库中有许多接口声明了一个抽象方法;一些这样的接口是:
// in java.lang package
interface Runnable { void run(); }
// in java.util package
interface Comparator<T> { boolean compare(T x, T y); }
// java.awt.event package:
interface ActionListener { void actionPerformed(ActionEvent e); }
// java.io package
interface FileFilter { boolean accept(File pathName); }
Java 8 引入了“函数式接口”的概念,将这一思想形式化。一个函数式接口只指定一个抽象方法。因为函数式接口只指定了一个抽象方法,所以它们有时被称为单一抽象方法(SAM)类型或接口。
注意:函数式接口可以接受通用参数,如上面例子中的Comparator<T>
和Callable<T>
接口。我们将在下一章介绍泛型(第四章)。
一个函数式接口的声明产生了一个可以和 lambda 表达式一起使用的“函数式接口类型”。此外,在 Java 8 中引入的
java.util.function
和java.util.stream
包中广泛使用了函数式接口。鉴于这个主题的重要性,你可以预期在你的 OCPJP 8 考试中会有许多与功能接口相关的问题。
对于被视为函数式接口的接口,它应该只有一个抽象方法。但是,它可能定义了任意数量的默认或静态方法。让我们看几个 Java 库中的例子来理解这一点。
下面是java.util.function.IntConsumer
接口的定义(不带注释和 javadoc 注释):
public interface IntConsumer {
void accept(int value);
default IntConsumer andThen(IntConsumer after) {
Objects.requireNonNull(after);
return (int t) -> { accept(t); after.accept(t); };
}
}
虽然这个接口有两个成员,但是andThen
方法是默认方法,只有accept
方法是抽象方法。因此,IntConsumer
界面是一个功能性界面。
再举一个例子,java.util.function.Predicate
是一个函数式接口,因为它只有一个抽象方法:
boolean test(T t)
但是需要注意的是Predicate
也有以下默认的方法定义:
default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> negate()
default Predicate<T> or(Predicate<? super T> other)
此外,它还定义了一个静态方法isEqual
:
static <T> Predicate<T> isEqual(Object targetRef)
给定所有这些方法定义,Predicate
仍然是一个函数式接口,因为它只有一个抽象方法test
。
@FunctionalInterface 批注
Java 编译器推断任何具有单一抽象方法的接口都是函数式接口。但是,您可以用@FunctionalInterface
注释标记函数式接口来确认这一点。推荐的做法是为函数式接口提供@FunctionalInterface
,因为有了这个注释,编译器可以给出更好的错误/警告。
这里有一个使用@FunctionalInterface
的例子,它有一个抽象方法,所以它可以干净地编译:
@FunctionalInterface
public abstract class AnnotationTest {
abstract int foo();
}
这个怎么样?
@FunctionalInterface
public interface AnnotationTest {
default int foo() {};
}
它会导致编译器错误“在接口中没有找到抽象方法”,因为它只提供了一个默认方法,而没有任何抽象方法。这个怎么样?
@FunctionalInterface
public interface AnnotationTest { /* no methods provided */ }
此接口没有任何方法。因为它缺少一个抽象方法,但是用@FunctionalInterface
进行了注释,所以会导致编译器错误。
这是另一种变化:
@FunctionalInterface
public interface AnnotationTest {
int foo();
int bar();
}
这段代码还会导致编译器错误“发现多个非重写抽象方法”,因为当一个函数式接口要求恰好提供一个抽象方法时,它有多个抽象方法。
Methods from Object Class in Functional Interfaces
根据 Java 语言规范(8.0 版),“接口不从 Object 继承,而是隐式声明许多与 Object 相同的方法。”如果你在接口中提供一个来自Object
类的抽象方法,它仍然是一个功能接口。
例如,考虑声明两个抽象方法的Comparator
接口:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
// other methods are default methods or static methods and are elided
}
这个接口是一个函数式接口,尽管它声明了两个抽象方法:compare()
和equals()
方法。当它有两个抽象方法时,它是一个怎样的函数式接口?因为equals()
方法签名与Object
匹配,而compare()
方法是唯一剩下的抽象方法,因此Comparator
接口是一个函数式接口。
这个接口定义怎么样?
@FunctionalInterface
interface EqualsInterface {
boolean equals(Object obj);
}
编译器给出错误:“EqualsInterface is not a functional interface: no abstract method found in interface EqualsInterface
”。为什么呢?因为方法equals
来自Object
,所以它不被认为是一个函数式接口。
要记住的要点
以下是一些关于功能接口的要点,对你参加 OCPJP 八级考试有帮助:
- 用
@FunctionalInterface
标注功能接口。否则,如果函数式接口不正确(例如,它有两个抽象方法),编译器将不会发出任何错误。 - 您只能对接口使用
@FunctionalInterface
注释,而不能对类、枚举等使用。 - 如果派生接口只有一个抽象方法或者只继承一个抽象方法,那么它可以是函数式接口。
- 对于函数式接口,在接口中声明来自
Object
类的方法不算抽象方法。
λ函数
认证目标 |
---|
创建和使用 Lambda 表达式 |
Java 8 中主要的新语言特性之一是 lambda 函数。事实上,这是自 Java 1 发布以来最大的变化之一。Lambdas 广泛用于编程语言世界,包括编译到 Java 平台的语言。例如,Groovy 语言可以编译到 Java 平台,并且对 lambda 函数(也称为闭包)有很好的支持。Oracle 决定通过 Java 8 将 lambdas 引入 JVM 上的主流语言——Java 语言本身。
Lambda Function Related Changes in Java 8
lambdas 的引入需要语言、库和 VM 实现的协调变化:
- 用于定义 lambda 函数的箭头操作符(“-->”),用于方法引用的双冒号操作符(“::”),以及关键字
default
- streams 库以及收藏库与 streams 的集成
- Lambda 函数是使用 Java 7 中引入的
invokedynamic
指令实现的
为了支持在语言中引入 lambdas,Java 8 中的类型推理也得到了加强。Lambdas 使库作者能够在库中创建并行算法,以利用现代硬件(即多核)中固有的并行性。
在 Java 8 中,java.util
已经通过使用 lambda 函数得到了很大的增强,我们将在下一章讨论这一点(第四章)。Java 8 增加了两个新的包java.util.function
和java.util.streams
。我们将在第五章的中的java.util.function
和第六章的中的java.util.streams
(称为流 API)中讨论类型。
Lambdas 可以极大地改变你设计和编写代码的方式。为什么呢?lambda 支持函数式编程范式——这意味着学习和使用 lambda 将意味着范式的转变。但是你不需要担心做出重大的改变——Java 无缝地将功能性与现有的面向对象特性集成在一起,你可以逐渐地在你的程序中使用越来越多的功能性特性。
在函数式编程范式中,lambda 函数可以存储在变量中,作为参数传递给其他函数,或者从其他函数返回,就像原始类型和引用变量一样。因为“lambda 函数”是可以传递的代码片段,所以可以认为函数范式支持“代码作为数据”传递“可执行代码段”的能力增强了 Java 的表达能力。
Lambda 函数:语法
lambda 函数由可选参数、箭头标记和主体组成:
LambdaParameters -> LambdaBody
LambdaParameters
lambda 函数的参数在左括号“(”和右括号“)”内传递。当传递多个参数时,它们用逗号分隔。- 箭头运算符。为了支持 lambdas,Java 引入了新的运算符“->”,也称为 lambda 运算符或 arrow 运算符。这个箭头操作符是必需的,因为我们需要从语法上将参数从主体中分离出来。
LambdaBody
可以是表达式,也可以是块。主体可以由单个语句组成(在这种情况下,不需要定义块的显式花括号);这样的λ体被称为“表达式λ”如果一个 lambda 体中有很多语句,它们需要在一个代码块中;这种λ体被称为“块λ”
编译器对 lambda 表达式执行类型推断:
- 如果没有在 lambda 函数定义中指定类型参数,编译器会推断参数的类型。指定参数类型时,需要指定全部或不指定;否则你会得到一个编译错误。
- 如果只有一个参数,可以省略括号。但是在这种情况下,您不能显式提供类型。您应该让编译器来推断单个参数的类型。
- lambda 函数的返回类型是从主体中推断出来的。如果 lambda 中的任何代码返回值,那么所有路径都应该返回值;否则你会得到一个编译错误。
有效 lambda 表达式的一些示例(假设相关的函数式接口可用):
(int x) -> x + x
x -> x % x
() -> 7
(int arg1, int arg2) -> (arg1 + arg2) / (arg1 – arg2)
无效 lambda 表达式的示例:
-> 7
// if no parameters, then empty parenthesis () must be provided
(arg1, int arg2) -> arg1 / arg2``// if argument types are provided, then it should be should be provided
λ函数—一个例子
让我们从一个简单的 lambda 函数的“hello world”示例开始(清单 3-11 )。
Listing 3-11. FirstLambda.java
interface LambdaFunction {
void call();
}
class FirstLambda {
public static void main(String []args) {
LambdaFunction lambdaFunction = () -> System.out.println("Hello world");
lambdaFunction.call();
}
}
执行时,该程序打印
Hello world
在这个程序中,接口LambdaFunction
声明了一个名为call()
的抽象方法;因此,它是一个功能接口。在FirstLambda
类的main
方法中,一个 lambda 函数被分配给函数式接口类型LambdaFunction
的一个变量。
LambdaFunction lambdaFunction = () -> System.out.println("Hello world");
这里,表达式() -> System.out.println("Hello world")
是一个λ表达式:
- 语法
()
表示没有参数。 - 箭头操作符
"->"
将方法参数与 lambda 主体分开。 - 语句
System.out.println("Hello world")
是 lambda 表达式的主体。
lambda 表达式和函数式接口LambdaFunction
有什么关系?它是通过LambdaFunction
接口内的单一抽象方法:void call()
。此抽象方法的签名和 lambda 表达式必须匹配:
- lambda 表达式有
()
表示它没有参数——它与不带参数的call
方法相匹配。 - 语句
System.out.println("Hello world")
是 lambda 表达式的主体。这个主体充当 lambda 函数的实现。 - 在这个 lambda 表达式体中没有 return 语句,因此编译器将这个表达式的返回类型推断为
void
类型,这与call
方法的返回类型相匹配。
下一条语句lambdaFunction.call();
调用 lambda 函数。此函数调用的结果是,控制台上会显示“Hello world”。
为什么 lambda 函数的函数类型要与给定函数式接口中抽象方法的函数类型相匹配?它用于类型检查。如果类型不匹配,您将得到一个编译器错误,如下所示:
LambdaFunction lambdaFunction = (``int i
因为这个 lambda 表达式有一个整数参数,但是LambdaFunction
中的call()
方法不接受任何参数,所以编译器给出一个错误:“incompatible types: incompatible parameter types in lambda expression"
”。
兰姆达斯街区
块 lambda 包含在“{”和“}”内的代码块中,如:
LambdaFunction lambdaFunction = (
int i
) ->
{
System.out.println("Hello world");
当您想要在 lambda 主体中提供多个语句时,块 lambda 非常有用(在 lambda 表达式中,您只能有一个语句)。此外,在 block lambda 中,您可以提供一个显式的return
语句(参见清单 3-12 )。
Listing 3-12. BlockLambda.java
class BlockLambda {
interface LambdaFunction {
String intKind(int a);
}
public static void main(String []args) {
LambdaFunction lambdaFunction =
(int i) -> {
if((i % 2) == 0) return "even";
else return "odd";
};
System.out.println(lambdaFunction.intKind(10));
}
}
它打印:
even
在这段代码中,我们定义了一个块 lambda。我们从这个 lambda 块返回一个String
,并且使用了显式的返回语句。在定义 block lambdas 时,我们应该确保为所有路径都提供了 return 语句,就像在这种情况下一样(否则,您会得到一个编译器错误)。返回语句应该与对应函数式接口中定义的抽象方法的返回类型相匹配(这里是LambdaFunction
接口中intKind
函数的String
返回类型)。
匿名内部类与 Lambda 表达式
在 Java 8 之前,作为 Java 程序员,我们习惯于编写匿名内部类。清单 3-13 等同于之前的程序(清单 3-11 ,除了它使用匿名内部类代替 lambda 函数。
Listing 3-13. AnonymousInnerClass.java
interface Function {
void call();
}
class AnonymousInnerClass {
public static void main(String []args) {
Function function = new Function() {
public void call() {
System.out.println("Hello world");
}
};
function.call();
}
}
清单 3-11 和清单 3-12 中的功能是相同的,但是使用匿名内部类会导致冗长的代码,而 lambda 表达式是简洁的。
思考 lambdas 的一种方式是“匿名函数”或“未命名函数”:它们是没有名称的函数,不与任何类相关联。具体来说,它们不是定义它们的类的静态或实例成员。如果在 lambda 函数中使用
this
关键字,它指的是定义 lambda 的作用域中的对象。
有效最终变量
Lambda 函数可以引用封闭范围内的局部变量。该变量需要显式声明为 final,否则该变量将被视为有效的 final。Effectively final 意味着编译器将变量视为最终变量,如果我们试图在 lambda 函数或函数的其余部分中修改它,将会发出一个错误。lambdas 的这种行为类似于从本地和匿名类访问外部作用域中的变量。他们访问的变量实际上也被认为是最终变量(正如我们前面讨论的)。
这里有一个例子。你听说过“猪拉丁”吗?这是孩子们玩的一种游戏,通过改变单词或添加后缀来创造单词,以创造奇怪的发音。在这个例子中,让我们简单地给一个单词加上后缀“ay”(清单 3-14 )。
Listing 3-14. PigLatin.java
interface SuffixFunction {
void call();
}
class PigLatin {
public static void main(String []args) {
String word = "hello";
SuffixFunction suffixFunc = () -> System.out.println(word + "ay");
suffixFunc.call();
}
}
该程序打印:
helloay
在 lambda 表达式中,我们使用了局部变量word
。因为在 lambda 表达式中使用,这个变量被认为是final
(尽管它没有被明确声明为final
)。尝试这段代码,它有一个附加语句分配给suffix
(甚至在调用 lambda 函数之前):
String word = "hello";
SuffixFunction suffixFunc = () -> System.out.println(word + "ay");
word = "e";
suffixFunc.call();
编译器对这段代码发出一个错误:
PigLatin.java:11: error: local variables referenced from a lambda expression must be final or effectively final
SuffixFunction suffixFunc = () -> System.out.println(word + "ay");
^
1 error
这是因为后缀在“e”被初始化后就被赋给了它,因此编译器不能把它当作最终变量。
为什么局部变量在 lambda 表达式中被访问时被认为是 final?原因是这种变异不是线程安全的。
请注意,这一限制不适用于数据成员和类成员。因此,当多个线程同时修改 lambda 表达式中的变量时,您可能会面临风险。此外,effectively final 仅适用于引用,而不适用于引用所指向的值。因此,您可以从 lambda 函数改变局部数组中的值——这不安全,但却是可能的。
要记住的要点
以下是关于 lambda 函数的一些要点,对你参加 OCPJP 8 考试有帮助:
- Lambda 表达式只能出现在可以进行赋值、函数调用或强制转换的上下文中。
- lambda 函数被视为嵌套块。因此,就像嵌套块一样,我们不能在封闭块中声明与局部变量同名的变量。
- Lambda 函数必须从所有分支返回值——否则会导致编译器错误。
- 当声明参数类型时,lambda 被称为“显式类型化”;如果它们是推断出来的,那么它就是“隐式类型化的”
- 如果 lambda 表达式抛出异常会发生什么?如果它是一个检查过的异常,那么函数式接口中的方法应该声明;否则会导致编译错误。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
开发使用抽象类和方法的代码
- 指定支持的功能的抽象,但不公开更精细的细节。
- 您不能创建抽象类的实例。
- 抽象类支持运行时多态性,而运行时多态性又支持松散耦合。
开发使用 final 关键字的代码
- final 类是不可继承的类(即,您不能从 final 类继承)。
- final 方法是不可重写的方法(即子类不能重写 final 方法)。
- final 类的所有方法都是隐式 final 的(即不可重写)。
- final 变量只能赋值一次。
创建内部类,包括静态内部类、局部类、嵌套类和匿名内部类
- Java 支持四种类型的嵌套类:静态嵌套类、内部类、局部内部类和匿名内部类。
- 静态嵌套类可能有静态成员,而其他类型的嵌套类则没有。
- 静态嵌套类和内部类可以访问外部类的成员(甚至私有成员)。但是,静态嵌套类只能访问外部类的静态成员。
- 局部类(局部内部类和匿名内部类)可以访问外部作用域中声明的所有变量(无论是方法、构造函数还是语句块)。
使用枚举类型,包括枚举类型中的方法和构造函数
- 枚举是实现用户受限输入的一种类型安全的方法。
- 即使在枚举定义内,也不能对枚举使用 new。
- 默认情况下,枚举类是最终类。
- 所有枚举类都是从
java.lang.Enum
隐式派生的。
开发声明、实现和/或扩展接口的代码,并使用 atOverride 注释
- 一个接口可以有三种方法:抽象方法、默认方法和静态方法。
- 当派生类型继承了基类型中具有相同签名的两个方法定义时,就会出现“菱形问题”。
- 如果两个超接口有相同的方法名,其中一个有定义,编译器会发出错误;这种冲突必须手动解决。
- 如果基类和基接口用相同的签名定义方法,则使用类中的方法定义,忽略接口定义。
- 一个函数式接口只包含一个抽象方法,但是可以包含任意数量的默认或静态方法。
- 函数式接口的声明会产生一个“函数式接口类型”,它可以与 lambda 表达式一起使用。
- 对于函数式接口,在接口中声明来自
Object
类的方法不算抽象方法。
创建和使用 Lambda 表达式
- 在 lambda 表达式中,左边的->提供参数;右边,身体。箭头运算符(“-->”)有助于简化 lambda 函数的表达式。
- 您可以创建对函数式接口的引用,并为其分配 lambda 表达式。如果从该接口调用抽象方法,它将调用指定的 lambda 表达式。
- 如果省略,编译器可以执行 lambda 参数的类型推断。声明时,参数可以有修饰符,如
final
。 - lambda 函数访问的变量实际上被认为是最终变量。
Question TimeWhich ONE of the following statements is TRUE? You cannot extend a concrete class and declare that derived class abstract You cannot extend an abstract class from another abstract class An abstract class must declare at least one abstract method in it You can create an instance of a concrete subclass of an abstract class but cannot create an instance of an abstract class itself Choose the correct answer based on the following class definition: public abstract final class Shape { }
Compiler error: a class must not be empty Compiler error: illegal combination of modifiers abstract and final Compiler error: an abstract class must declare at least one abstract method No compiler error: this class definition is fine and will compile successfully Choose the best option based on this program: class Shape {
public Shape() {
System.out.println("Shape constructor");
}
public class Color {
public Color() {
System.out.println("Color constructor");
}
}
}
class TestColor {
public static void main(String []args) {
Shape.Color black = new Shape().Color(); // #1
}
}
Compiler error: the method Color() is undefined for the type Shape Compiler error: invalid inner class Works fine: Shape constructor, Color constructor Works fine: Color constructor, Shape constructor Choose the best option based on this program: class Shape {
private boolean isDisplayed;
protected int canvasID;
public Shape() {
isDisplayed = false;
canvasID = 0;
}
public class Color {
public void display() {
System.out.println("isDisplayed: "+isDisplayed);
System.out.println("canvasID: "+canvasID);
}
}
}
class TestColor {
public static void main(String []args) {
Shape.Color black = new Shape().new Color();
black.display();
}
}
Compiler error: an inner class can only access public members of the outer class Compiler error: an inner class cannot access private members of the outer class Runs and prints this output: isDisplayed: false canvasID: 0 Compiles fine but crashes with a runtime exception Determine the behavior of this program: public class EnumTest {
PrinterType printerType;
enum PrinterType {INKJET, DOTMATRIX, LASER};
public EnumTest(PrinterType pType) {
printerType = pType;
}
public static void main(String[] args) {
PrinterType pType = new PrinterType();
EnumTest enumTest = new EnumTest(PrinterType.LASER);
}
}
Prints the output printerType:LASER Compiler error: enums must be declared static Compiler error: cannot instantiate the type EnumTest.PrinterType This program will compile fine, and when run, will crash and throw a runtime exception Is the enum definition given below correct? public enum PrinterType {
private int pagePrintCapacity; // #1
DOTMATRIX(5), INKJET(10), LASER(50); // #2
private PrinterType(int pagePrintCapacity) {
this.pagePrintCapacity = pagePrintCapacity;
}
public int getPrintPageCapacity() {
return pagePrintCapacity;
}
}
Yes, this enum definition is correct and will compile cleanly without any warnings or errors No, this enum definition is incorrect and will result in compile error(s) No, this enum definition will result in runtime exception(s) Yes, this enum definition is correct but will compile with warnings. Determine the behavior of this program: interface DoNothing {
default void doNothing() { System.out.println("doNothing"); }
}
@FunctionalInterface
interface DontDoAnything extends DoNothing {
@Override
abstract void doNothing();
}
class LambdaTest {
public static void main(String []args) {
DontDoAnything beIdle = () -> System.out.println("be idle");
beIdle.doNothing();
}
}
This program results in a compiler error for DontDoAnything interface: cannot override default method to be an abstract method This program results in a compiler error: DontDoAnything is not a functional interface This program prints: doNothing This program prints: be idle Determine the behavior of this program: public class EnumTest {
public EnumTest() {
System.out.println("In EnumTest constructor ");
}
public void printType() {
enum PrinterType { DOTMATRIX, INKJET, LASER }
}
}
This code will compile cleanly without any compiler warnings or errors, and when used, will run without any problems This code will compile cleanly without any compiler warnings or errors, and when used, will generate a runtime exception This code will produce a compiler error: enum types must not be local This code will give compile-time warnings but not any compiler errors Determine the behavior of this program: interface BaseInterface {
default void foo() { System.out.println("BaseInterface's foo"); }
}
interface DerivedInterface extends BaseInterface {
default void foo() { System.out.println("DerivedInterface's foo"); }
}
interface AnotherInterface {
public static void foo() { System.out.println("AnotherInterface's foo"); }
}
public class MultipleInheritance implements DerivedInterface, AnotherInterface {
public static void main(String []args) {
new MultipleInheritance().foo();
}
}
This program will result in a compiler error: Redundant method definition for function foo This program will result in a compiler error in MultipleInheritance class: Ambiguous call to function foo The program prints: DerivedInterface’s foo The program prints: AnotherInterface's foo Determine the behavior of this program: class LambdaFunctionTest {
@FunctionalInterface
interface LambdaFunction {
int apply(int j);
boolean equals(java.lang.Object arg0);
}
public static void main(String []args) {
LambdaFunction lambdaFunction = i -> i * i; // #1
System.out.println(lambdaFunction.apply(10));
}
}
This program results in a compiler error: interfaces cannot be defined inside classes This program results in a compiler error: @FunctionalInterface used for LambdaFunction that defines two abstract methods This program results in a compiler error in code marked with #1: syntax error This program compiles without errors, and when run, it prints 100 in console
答案:
D. You can create an instance of a concrete subclass of an abstract class but cannot create an instance of an abstract class itself B. Compiler error: illegal combination of modifiers abstract and final You cannot declare an abstract class final since an abstract class must to be extended. Class can be empty in Java, including abstract classes. An abstract class can declare zero or more abstract methods. A. Compiler error: The method Color() is undefined for the type Shape You need to create an instance of outer class Shape in order to create an inner class instance, as in new Shape().new Color();.
C. Runs and prints this output: isDisplayed: false canvasID: 0 An inner class can access all members of an outer class, including the private members of the outer class. C. Compiler error: cannot instantiate the type EnumTest.PrinterType You cannot instantiate an enum type using new. B. No, this enum definition is incorrect and will result in compile error(s) You need to define enum elements first before any other attribute in an enum class. In other words, this enum definition will compile cleanly if you interchange the statements marked with “#1” and “#2” within comments in this code. D. This program prints: be idle A default method can be overridden in a derived interface and can be made abstract. DoNothing is a functional interface because it has an abstract method. The call beIdle.doNothing() calls the System.out.println given inside the lambda expression and hence it prints “be idle” on the console. C. It will produce a compiler error: enum types must not be local An enum can only be defined inside of a top-level class or interface and not within a method. C. The program prints: DerivedInterface’s foo A default method can be overridden. Since DerivedInterface extends BaseInterface, the default method definition for foo is overridden in the DerivedInterface. Static methods do not cause conflicting definition of foo since they are not overridden, and they are accessed using the interface name, as in AnotherInterface.foo. Hence, the program compiles without errors. In the main method within MultipleInheritance class, the overridden foo method is invoked and hence the call resolves to foo method defined in DerivedInterface. D. This program compiles without errors, and when run, it prints 100 in console An interface can be defined inside a class. The signature of the equals method matches that of the equal method in Object class; hence it is not counted as an abstract method in the functional interface. It is acceptable to omit the parameter type when there is only one parameter and the parameter and return type are inferred from the LambdaFunction abstract method declaration int apply(int j). Since the lambda function is passed with the value 10, the returned value is 10 * 10, and hence 100 is printed in console.
四、泛型和集合
认证目标 |
---|
创建和使用泛型类 |
创建和使用 ArrayList、TreeSet、TreeMap 和 ArrayDeque 对象 |
使用 java.util.Comparator 和 java.lang.Comparable 接口 |
集合流和过滤器 |
使用流和列表的 forEach 方法进行迭代 |
描述流接口和流管道 |
使用 lambda 表达式筛选集合 |
对流使用方法引用 |
每个重要的 Java 应用都使用数据结构和算法。Java 集合的框架提供了大量易于使用的通用数据结构和算法。这些数据结构和算法可以以类型安全的方式用于任何合适的数据类型;这是通过使用一种称为泛型的语言特性来实现的。
Java 中的集合实现数据结构,算法使用泛型和 lambda 函数实现。因此,这些主题在 1Z0-809 考试大纲中被合并为一个主题。在这一章中,我们从讨论泛型开始。因为我们的经验表明,正确回答关于泛型的问题通常很棘手,所以我们将详细讨论泛型。接下来我们讨论重要的集合,也讨论java.lang.Comparator
和java.lang.Comparable
接口。最后,我们将详细介绍如何在 Java 集合框架中使用 lambda 函数和流。你可能会在 OCPJP 8 考试中遇到许多关于泛型、集合和流的问题,所以本章提供了考试主题的详细内容。
创建和使用泛型类
认证目标 |
---|
创建和使用泛型类 |
泛型是 Java 1.5 版中引入的一种语言特性。在 Java 中引入泛型之前,Object
基类被用作泛型的替代。使用泛型,你为一种类型(比如说T
)编写适用于所有类型的代码,而不是为每种类型编写单独的类。让我们从一个简单的例子开始。
假设您想要打印方括号内的对象值。例如,要打印一个值为 10 的Integer
对象,而不是将“10”打印到控制台,您希望在一个“框”内打印该值,如下所示:“[10]”。清单 4-1 包含了一个通用版本的BoxPrinter
类。
Listing 4-1. BoxPrinterTest.java
// This program shows container implementation using generics
class BoxPrinter<T> {
private T val;
public BoxPrinter(T arg) {
val = arg;
}
public String toString() {
return "[" + val + "]";
}
}
class BoxPrinterTest {
public static void main(String []args) {
BoxPrinter<Integer> value1 = new BoxPrinter<Integer>(new Integer(10));
System.out.println(value1);
BoxPrinter<String> value2 = new BoxPrinter<String>("Hello world");
System.out.println(value2);
}
}
它打印以下内容:
[10]
[Hello world]
这里有很多需要注意的地方。
See the declaration of BoxPrinter
: class BoxPrinter<T>
You gave the BoxPrinter
class a type placeholder <T>
—the type name T
within angle brackets “” following the class name. You can use this type name inside the class to indicate that it is a placeholder for the actual type to be provided later. Inside the class you first use T
in field declaration: private T val;
You are declaring val
of the generic type—the actual type will be specified later when you use BoxPrinter
. In main()
, you declare a variable of type BoxPrinter
for an Integer
like this: BoxPrinter<Integer> value1
Here, you are specifying that T is of type Integer—identifier T (a placeholder) is replaced with the type Integer. So, the val inside BoxPrinter becomes Integer because T gets replaced with Integer. Now, here is another place where you use T:
public BoxPrinter(T arg) {
val = arg;
}
类似于类型为T
的val
的声明,您是说BoxPrinter
构造函数的参数属于类型T
。稍后在main()
方法中,当在new
中调用构造函数时,指定T
的类型为Integer
:
new BoxPrinter<Integer>(new Integer(10));
现在,在BoxPrinter
构造函数中,arg
和val
应该属于同一类型,因为它们都属于类型T
。例如,如果您按如下方式更改构造函数:
new BoxPrinter<String>(new Integer(10));
BoxPrinter
的类型是String
,传递的参数的类型是Integer
,所以在使用泛型时,您会得到一个类型不匹配的编译器错误(这很好,因为您会更早发现问题)。
让我们考虑另一个例子。这里有一个Pair
泛型类,可以保存两种不同类型的对象,T1
和T2
(清单 4-2 )。
Listing 4-2. PairTest.java
// It demonstrates the usage of generics in defining classes
class Pair<T1, T2> {
T1 object1;
T2 object2;
Pair(T1 one, T2 two) {
object1 = one;
object2 = two;
}
public T1 getFirst() {
return object1;
}
public T2 getSecond() {
return object2;
}
}
class PairTest {
public static void main(String []args) {
Pair<Integer, String> worldCup = new Pair<Integer, String>(2018, "Russia");
System.out.println("World cup " + worldCup.getFirst() +
" in " + worldCup.getSecond());
}
}
该程序打印以下内容:
World cup 2018 in Russia
这里的T1
和T2
是类型持有者。您可以在尖括号内给出这些类型占位符:<T1, T2>
。当使用Pair
类时,您必须指定您将使用哪些特定类型来代替T1
和T2
。例如,您使用Integer
和String
来表示Pair
,就像在main()
方法中的Pair<Integer, String>
一样。现在,假设Pair
类有这样的主体:
// how Pair<Integer, String> can be treated internally
class Pair {
Integer object1;
String object2;
Pair(Integer one, String two) {
object1 = one;
object2 = two;
}
public Integer getFirst() {
return object1;
}
public String getSecond() {
return object2;
}
}
换句话说,尝试手动查找并替换类型占位符,并用代码中的实际类型替换它们。这将帮助您理解泛型实际上是如何工作的。这样,您就可以理解getFirst()
和getSecond()
方法如何在main()
方法中返回Integer
和String
值。
在声明中
Pair<Integer, String> worldCup = new Pair<Integer, String>(2018, "Russia");
请注意,类型完全匹配。如果你尝试
Pair<Integer, String> worldCup = new Pair<String, String>(2018, "Russia");
您将得到以下编译器错误:
TestPair.java:20: cannot find symbol
symbol : constructor Pair(int,java.lang.String)
location: class Pair<java.lang.String,java.lang.String>
现在,试试这个说法怎么样?
Pair<Integer, String> worldCup = new Pair<Number, String>(2018, "Russia");
您将得到另一个编译器错误,因为声明的类型worldCup
与初始化表达式中给定的类型不匹配:
TestPair.java:20: incompatible types
found : Pair<java.lang.Number,java.lang.String>
required: Pair<java.lang.Integer,java.lang.String>
现在修改通用的Pair
类。Pair<T1, T2>
存储类型为T1
和T2
的对象。采用类型T
并存储该类型的两个对象T
的通用 pair 类怎么样?显然,一种方法是用相同的类型实例化Pair<T1, T2>
,比如说Pair<String, String>
,但这不是一个好的解决方案。为什么呢?没有办法确保用相同的类型实例化Pair
!清单 4-3 是Pair
的修改版——姑且称之为PairOfT
——它采用了一个类型占位符T
。
Listing 4-3. PairOfT.java
// This program shows how to use generics in your programs
class PairOfT<T> {
T object1;
T object2;
PairOfT(T one, T two) {
object1 = one;
object2 = two;
}
public T getFirst() {
return object1;
}
public T getSecond() {
return object2;
}
}
现在,这种说法行得通吗?
PairOfT<Integer, String> worldCup = new PairOfT<Integer, String>(2018, "Russia");
不,因为PairOfT
有一个类型参数,而你在这里给了两个类型参数。所以,你会得到一个编译错误。那么,这个说法怎么样?
PairOfT<String> worldCup = new PairOfT<String>(2018, "Russia");
不,您仍然会得到一个编译器错误:
TestPair.java:20: cannot find symbol
symbol : constructor PairOfT(int,java.lang.String)
location: class PairOfT<java.lang.String>
PairOfT<String> worldCup = new PairOfT<String>(2018, "Russia");
原因是 2018 年——当被装箱时——是一个Integer
,你应该给出一个String
作为自变量。这个说法怎么样?
PairOfT<String> worldCup = new PairOfT<String>("2018", "Russia");
是的,它可以编译并且运行良好。
菱形语法
在上一节中,我们讨论了如何创建泛型类型实例,如以下语句所示:
Pair<Integer, String> worldCup = new Pair<Integer, String>(2018, "Russia");
我们还讨论了如果这些类型不匹配,编译器会如何出错,如下面的语句所示,它不会编译:
Pair<Integer, String> worldCup = new Pair<String, String>(2018, "Russia");
看看确保在声明类型(本例中为Pair<Integer, String>
)和新对象创建表达式(本例中为new Pair<String, String>()
)中提供相同类型的参数有多繁琐?
为了简化您的生活,Java 1.7 引入了 diamond 语法,其中可以省略类型参数:您可以让编译器从类型声明中推断类型。因此,声明可以简化为
Pair<Integer, String> worldCup = new Pair<>(2018, "Russia");
为了清楚起见,清单 4-4 包含了使用这个菱形语法的完整程序。
Listing 4-4. TestPair.java
// This program shows the usage of the diamond syntax when using generics
class Pair<T1, T2> {
T1 object1;
T2 object2;
Pair(T1 one, T2 two) {
object1 = one;
object2 = two;
}
public T1 getFirst() {
return object1;
}
public T2 getSecond() {
return object2;
}
}
class TestPair {
public static void main(String []args) {
Pair<Integer, String> worldCup = new Pair<>(2018, "Russia");
System.out.println("World cup " + worldCup.getFirst() +
" in " + worldCup.getSecond());
}
}
该程序将干净地编译并打印以下语句:
World cup 2018 in Russia
注意,忘记初始化表达式中的菱形操作符< >
是一个常见的错误,如
Pair<Integer, String> worldCup = new Pair(2018, "Russia");
下面是您将从编译器得到的警告(当您将命令行选项-Xlint:unchecked
传递给 javac 时):
Pair.java:19: warning: [unchecked] unchecked call to Pair(T1,T2) as a member of the raw type Pair
Pair<Integer, String> worldCup = new Pair(2018, "Russia");
^
where T1,T2 are type-variables:
T1 extends Object declared in class Pair
T2 extends Object declared in class Pair
Pair.java:19: warning: [unchecked] unchecked conversion
Pair<Integer, String> worldCup = new Pair(2018, "Russia");
^
required: Pair<Integer,String>
found: Pair
2 warnings
由于Pair
是一个泛型类型,并且您忘记了使用<>
或显式提供类型参数,编译器将其视为一个原始类型,其中Pair
带有两个Object
类型参数。尽管这种行为在这个特定的代码段中没有引起任何问题,但它是危险的,可能会导致错误,如下一节所示。
原始类型和泛型类型的互操作性
可以使用泛型类型而不指定其关联类型。在这种情况下,该类型被称为原始类型。例如,List<T>
应该与关联的类型一起使用,即List<String>
;但是,它可以在不指定伴随类型的情况下使用,即仅使用List
。在后一种情况下,List
被称为原始类型。
当您使用原始类型时,您将失去泛型提供的类型安全优势。例如,类型Vector
是一个原始类型。原始类型绕过编译时的类型检查;然而,它们可能抛出运行时异常(例如,ClassCastException
)。因此,不建议在新代码中使用原始类型。
好了,现在你明白你不应该使用原始类型。但是,你可能会问,为什么编译器本身不为这样的类型声明抛出错误?答案是向后兼容。Java 1.5 中引入了 Java 泛型。Java 支持原始类型,以使基于泛型的代码与遗留代码兼容。但是,强烈建议您不要在代码中使用原始类型。
为什么呢?如果将原始类型与泛型一起使用,会发生什么?让我们在清单 4-5 中使用这两种类型,并检查效果。
Listing 4-5. RawTest1.java
import java.util.List;
import java.util.LinkedList;
import java.util.Iterator;
class RawTest1 {
public static void main(String []args) {
List list = new LinkedList();
list.add("First");
list.add("Second");
List<String> strList = list; //#1
for(Iterator<String> itemItr = strList.iterator(); itemItr.hasNext();)
System.out.println("Item: " + itemItr.next());
List<String> strList2 = new LinkedList<>();
strList2.add("First");
strList2.add("Second");
List list2 = strList2; //#2
for(Iterator<String> itemItr = list2.iterator(); itemItr.hasNext();)
System.out.println("Item: " + itemItr.next());
}
}
你对上述计划有什么期望?你认为它能正确编译/执行吗?嗯,是的——它将编译(有警告)并执行,没有任何问题。它打印以下内容:
Item: First
Item: Second
Item: First
Item: Second
清单 4-6 引入了一些变化;观察输出。
Listing 4-6. RawTest2.java
import java.util.List;
import java.util.LinkedList;
import java.util.Iterator;
class RawTest2 {
public static void main(String []args) {
List list = new LinkedList();
list.add("First");
list.add("Second");
List<String> strList = list;
strList.add(10); // #1: generates compiler error
for(Iterator<String> itemItr = strList.iterator(); itemItr.hasNext();)
System.out.println("Item : " + itemItr.next());
List<String> strList2 = new LinkedList<>();
strList2.add("First");
strList2.add("Second");
List list2 = strList2;
list2.add(10); // #2: compiles fine, results in runtime exception
for(Iterator<String> itemItr = list2.iterator(); itemItr.hasNext();)
System.out.println("Item : " + itemItr.next());
}
}
在上面的示例中,您添加了两条语句。第一个声明如下:
strList.add(10); // #1: generates compiler error
您试图在一个List<String>
类型列表中添加一个整数项,因此您得到一个编译时错误"no suitable method found for add(int)"
。如前所述,这种编译器级别的检查是好的,因为如果没有它,稍后可能会导致运行时异常。这是您添加的第二条语句:
list2.add(10); // #2: compiles fine, results in runtime exception
这里,list2
链表(原始类型)用一个泛型类型List<String>
初始化。初始化之后,您在 list raw 类型中添加了一个整数。这是允许的,因为list2
是一个原始类型。但是,会产生一个ClassCastException
。
我们从这个例子中学到的教训是避免在我们的程序中混合原始类型和泛型类型,因为这可能导致运行时的错误行为。如果您需要在程序中同时使用这两种类型,请确保在容器中添加单一类型的项,并使用相同的类型进行检索。
避免将原始类型与泛型类型混合。
通用方法
与泛型类类似,您可以创建泛型方法,即采用泛型参数类型的方法。泛型方法对于编写适用于多种类型而功能保持不变的方法非常有用。例如,java.util.Collections
类中有许多泛型方法。
让我们实现一个名为fill()
的简单方法。给定一个容器,fill()
方法用值val
填充所有容器元素。清单 4-7 包含了Utilities
类中fill()
方法的实现。
Listing 4-7. UtilitiesTest.java
// This program demonstrates generic methods
import java.util.List;
import java.util.ArrayList;
class Utilities {
public static <T> void fill(List<T> list, T val) {
for(int i = 0; i < list.size(); i++)
list.set(i, val);
}
}
class UtilitiesTest {
public static void main(String []args) {
List<Integer> intList = new ArrayList<Integer>();
intList.add(10);
intList.add(20);
System.out.println("The original list is: " + intList);
Utilities.fill(intList, 100);
System.out.println("The list after calling Utilities.fill() is: " + intList);
}
}
它打印以下内容
The original list is: [10, 20]
The list after calling Utilities.fill() is: [100, 100]
让我们一步一步地看看这段代码:
You create a method named fill()
in the Utilities
class with this declaration: public static <T> void fill(List<T> list, T val)
You declare the generic type parameter T
in this method. After the qualifiers public and static, you put <T>
and then followed it by return type, method name, and its parameters. This declaration is different from generic classes—you give the generic type parameters after the class name in generic classes. In the body, you write the code as if it’s a normal method. for(int i = 0; i < list.size(); i++)
list.set(i, val);
You loop over the list from 0 until its size and set each of the elements to value val
in each iteration. You use the set()
method in List
, which takes the index position in the container as the first argument and the actual value to be set as the second argument. In the main()
method in the UtilitiesTest
class, this is how you call the fill()
method: Utilities.fill(intList, 100);
请注意,您没有显式给出泛型类型参数值。由于intList
是类型Integer
并且 100 被装箱为类型Integer
,编译器推断出fill()
方法中的类型T
是类型Integer
。
泛型和子类型
您可以将派生类型对象分配给其基类型引用;这就是你所谓的分型。但是,对于泛型,类型参数应该完全匹配;否则你会得到一个编译错误。换句话说,子类型对于泛型参数不起作用。是的,这是一个很难记住的规则,所以让我们更详细地讨论为什么子类型对于泛型类型参数不起作用。
子类型化适用于类类型:你可以将一个派生类型对象赋给它的基类引用。但是,子类型对泛型类型参数不起作用:不能将派生的泛型类型参数赋给基类型参数。
让我们看看,如果假设可以对泛型类型参数使用子类型,会出现什么问题。
// illegal code –``assume
List<Number> intList = new ArrayList<Integer>();
intList.add(new Integer(10)); // okay
intList.add(new Float(10.0f)); // oops!
List<Number>
类型的intList
应该保存一个数组列表ArrayList<Integer>
。这看起来很合理,因为List
延伸了ArrayList
,而Integer
延伸了Number
。但是,您最终可以在intList
中插入一个Float
值!回想一下,intList
的动态类型是ArrayList<Integer>
类型——所以您在这里违反了类型安全(因此将得到不兼容类型的编译器错误)。由于泛型旨在避免类似这样的类型安全错误,因此不能将派生的泛型类型参数赋给基类型参数。
正如您所看到的,泛型参数类型的子类型化是不允许的,因为它是不安全的——但它仍然是一个不方便的限制。幸运的是,Java 支持通配符参数类型,您可以在其中使用子类型。我们现在将探索这种能力。
泛型的类型参数有一个限制:对于赋值,泛型类型参数应该完全匹配。为了克服这个子类型问题,可以使用通配符类型。
通配符参数
在上一节中,您看到了子类型对于泛型类型参数不起作用。所以,
List<Number> intList = new ArrayList<Integer>();
给出编译器错误
WildCardUse.java:6: incompatible types
found : java.util.ArrayList<java.lang.Integer>
required: java.util.List<java.lang.Number>
List<Number> numList = new ArrayList<Integer>();
如果您稍微更改语句以使用通配符参数,它将会编译
List<?> wildCardList = new ArrayList<Integer>();
通配符是什么意思?就像你在卡牌游戏中用来替代任何一张牌的通配符(啊,玩卡牌游戏太好玩了!),您可以使用通配符来表示它可以匹配任何类型。对于List<?>
,你的意思是它是任何类型的List
——换句话说,你可以说它是一个“未知列表!”
但是等一下……当你想要一个表示“任何类型”的类型时,你使用Object
类,不是吗?同样的语句,但是使用了Object
类型参数,怎么样?
List<Object> numList = new ArrayList<Integer>();
运气不好——使用List<Number>
您会得到与上面相同的错误!
WildCardUse.java:6: incompatible types
found : java.util.ArrayList<java.lang.Integer>
required: java.util.List<java.lang.Object>
List<Object> numList = new ArrayList<Integer>();
换句话说,您仍然在尝试对泛型参数使用子类型化——但它仍然不起作用。如你所见,List<Object>
和List<?>
不一样。事实上,List<?>
是任何List
类型的超类型,这意味着你可以在List<?>
预期的地方通过List<Integer>
,或者List<String>
,甚至List<Object>
。
让我们在一个例子中使用通配符,看看它是否有效(参见清单 4-8 )。
Listing 4-8. WildCardUse.java
// This program demonstrates the usage of wild card parameters
import java.util.List;
import java.util.ArrayList;
class WildCardUse {
static void printList(List<?> list){
for(Object element: list)
System.out.println("[" + element + "]");
}
public static void main(String []args) {
List<Integer> list = new ArrayList<>();
list.add(10);
list.add(100);
printList(list);
List<String> strList = new ArrayList<>();
strList.add("10");
strList.add("100");
printList(strList);
}
}
该程序打印以下内容:
[10]
[100]
[10]
[100]
好了,它工作了,使用通配符的列表可以传递整数列表和字符串列表。发生这种情况是因为printList()
方法的参数类型— List<?>
。太好了!
通配符的限制
让我们考虑下面的代码片段,它试图添加一个元素并打印列表:
List<?> wildCardList = new ArrayList<Integer>();
wildCardList.add(new Integer(10));
System.out.println(wildCardList);
您会得到以下编译器错误:
WildCardUse.java:7: cannot find symbol
symbol : method add(java.lang.Integer)
location: interface java.util.List<capture#145 of ? extends java.lang.Number>
wildCardList.add(new Integer(10));
为什么呢?你绝对确定add()
方法存在于List
接口中。那编译器为什么不找方法呢?
这个问题需要一些详细的解释。当你使用通配符类型<?>
时,你对编译器说你忽略了类型信息,那么<?>代表未知类型。每当您试图将参数传递给泛型类型时,java 编译器都会尝试推断所传递参数的类型以及泛型的类型,并证明类型安全。现在,您试图使用add()
方法在列表中插入一个元素。因为wildCardList
不知道它持有哪种类型的对象,所以向它添加元素是有风险的。您最终可能会添加一个字符串,例如“hello ”,而不是一个整数值。为了避免这个问题(记住,语言中引入泛型是为了确保类型安全!),编译器不允许你调用修改对象的方法。因为add
方法修改了对象,所以您会得到一个错误!错误信息看起来也很混乱,就像在<capture#145 of ? extends java.lang.Number>
中一样。
一般来说,当你使用通配符参数时,你不能调用修改对象的方法。如果你试图修改,编译器会给出令人困惑的错误信息。但是,您可以调用访问该对象的方法。
要记住的要点
以下是一些可能对你的 OCPJP 八级考试有价值的建议:
- 即使类或接口本身不是泛型的,也可以在接口或类中定义或声明泛型方法。
- 不带类型参数的泛型类称为原始类型。当然,原始类型不是类型安全的。Java 支持原始类型,因此可以在比 Java 5 更早的代码中使用泛型类型(注意,泛型是在 Java 5 中引入的)。当您在代码中使用原始类型时,编译器会生成警告。您可以使用
@SuppressWarnings({ "unchecked" })
来抑制与原始类型相关的警告。 List<?>
是任何List
类型的超类型,这意味着你可以通过List<Integer>
,或者List<String>
,甚至是List<?>
预期的List<Object>
。- 泛型的实现本质上是静态的,这意味着 Java 编译器解释源代码中指定的泛型,并用具体类型替换泛型代码。这被称为类型擦除。编译后,代码看起来类似于开发人员用具体类型编写的代码。本质上,使用泛型有两个好处:首先,它引入了一个抽象,使您能够编写泛型实现;其次,它允许您编写具有类型安全的泛型实现。
- 由于类型擦除,泛型有许多限制。几个重要的例子如下:
- 不能使用 new 运算符实例化泛型类型。例如,假设 mem 是一个字段,下面的语句将导致编译器错误:
T mem = new T(); // wrong usage - compiler error
- 不能实例化泛型类型的数组。例如,假设 mem 是一个字段,下面的语句将导致编译器错误:
T[] amem = new T[100]; // wrong usage - compiler error
- 可以声明 T 类型的实例字段,但不能声明 T 类型的静态字段。例如,
class X<T> {
T instanceMem; // okay
static T statMem; // wrong usage - compiler error
}
- 不能使用 new 运算符实例化泛型类型。例如,假设 mem 是一个字段,下面的语句将导致编译器错误:
- 不可能有泛型异常类;因此,下面的代码不会被编译:
class GenericException<T> extends Throwable { } // wrong usage - compiler error
- 不能用基元类型实例化泛型类型——换句话说,
List<int>
不能被实例化。但是,您可以使用装箱的基本类型。
创建和使用集合类
认证目标 |
---|
创建和使用 ArrayList、TreeSet、TreeMap 和 ArrayDeque 对象 |
Java 库有一个集合框架,它大量使用泛型,并提供了一组容器和算法。在本节中,我们将重点介绍如何使用集合框架。具体来说,我们将讨论重要的集合类,包括ArrayList
、TreeSet
、TreeMap
和ArrayDeque
对象。
术语集合是一个通用术语,而
Collection
和Collections
是java.util
包的特定 API。Collections
——如在java.util.Collections
中一样——是一个只包含静态方法的实用程序类。通用术语集合指的是诸如映射、集合、堆栈和队列之类的容器。为了避免混淆,我们将在本章中使用容器这个术语来指代这些集合。
抽象类和接口
java.util
库中的类型层次由许多提供通用功能的抽象类和接口组成。表 4-1 列出了这个层次中的一些重要类型。我们将在本节稍后的部分更详细地讨论其中的一些类型。
表 4-1。
Important Abstract Classes and Interfaces in the Collections Framework
抽象类/接口 | 简短描述 |
---|---|
Iterable |
实现这个接口的类可以用来迭代一个foreach 语句。 |
Collection |
集合层次结构中类的公共基接口。当你想写非常通用的方法时,可以通过Collection 接口。例如,java.util.Collections 中的max() 方法接受一个Collection 并返回一个对象。 |
List |
存储元素序列的容器的基本接口。您可以使用索引来访问元素,并在以后检索相同的元素(以便它保持插入顺序)。您可以在一个List 中存储重复的元素。 |
Set 、SortedSet 、NavigableSet |
不允许重复元素的容器接口。SortedSet 按排序顺序维护集合元素。NavigableSet al lows 在集合中搜索最接近的匹配。 |
Queue ,Deque |
Queue 是容器的基本接口,包含一系列要处理的元素。例如,实现Queue 的类可以是 LIFO(后进先出—如堆栈数据结构)或 FIFO(先进先出—如队列数据结构)。在Deque 中,你可以在两端插入或移除元素。 |
Map 、SortedMap 、NavigableMap |
将键映射到值的容器的接口。在SortedMap 中,按键是有序排列的。一个NavigableMap 允许你搜索并返回给定搜索标准的最接近的匹配。注意,Map 层次没有扩展Collection 接口。 |
Iterator ,ListIterator |
如果一个类实现了Iterator 接口,你可以正向遍历容器。如果一个类实现了ListIterator 接口,你可以正向和反向遍历。 |
这些是相当多的基本类型,但不要被它们淹没。您将看到特定的具体类,并使用其中的一些基本类型。我们将只讨论Collection
接口,然后继续讨论特定的具体类,这些类是集合层次结构的一部分,在考试主题中会提到。
收集界面
Collection
接口提供了对所有容器通用的方法,如add()
和remove()
。表 4-2 列出了该接口中最重要的方法。在你使用它们之前,看一看它们。
表 4-2。
Important Methods in the Collection Interface
方法 | 简短描述 |
---|---|
boolean add(Element elem) |
将elem 添加到底层容器中。 |
void clear() |
从容器中移除所有元素。 |
boolean isEmpty() |
检查容器是否有任何元素。 |
Iterator<Element> iterator() |
返回一个用于遍历容器的Iterator<Element> 对象。 |
boolean remove(Object obj) |
如果容器中存在obj ,则移除该元素。 |
int size() |
返回容器中元素的数量。 |
Object[] toArray() |
返回一个包含容器中所有元素的数组。 |
根据底层容器的不同,add()
和remove()
等方法可能会失败。例如,如果容器是只读的,您将无法添加或移除元素。除了这些方法之外,Collection
接口中还有许多方法适用于容器中的多个元素(表 4-3 )。
表 4-3。
Methods in the Collection Interface That Apply to Multiple Elements
方法 | 简短描述 |
---|---|
boolean addAll(Collection<? extends Element> coll) |
将coll 中的所有元素添加到底层容器中。 |
boolean containsAll(Collection<?> coll) |
检查coll 中给出的所有元素是否都存在于底层容器中。 |
boolean removeAll(Collection<?> coll) |
从底层容器中移除也存在于coll 中的所有元素。 |
boolean retainAll(Collection<?> coll) |
仅当元素也出现在coll 中时,才保留底层容器中的元素;它会删除所有其他元素。 |
具体类
Collection
层次结构中的许多接口和抽象类提供了特定具体类实现/扩展的公共方法。具体的类提供了实际的功能,你只需要学习其中的一小部分就可以为 OCPJP 八级考试做好准备。表 4-4 总结了你应该知道的职业特点。
表 4-4。
Important Concrete Classes in Collection Framework
混凝土类 | 简短描述 |
---|---|
ArrayList |
在内部实现为可调整大小的数组。这是使用最广泛的具体类之一。搜索速度快,但插入或删除速度慢。允许重复。 |
LinkedList |
在内部实现双向链表数据结构。插入或删除元素很快,但搜索元素很慢。另外,当你需要一个堆栈(LIFO)或队列(FIFO)数据结构时,可以使用LinkedList 。允许重复。 |
HashSet |
在内部实现为哈希表数据结构。用于存储一组元素—它不允许存储重复的元素。快速搜索和检索元素。它不维护存储元素的任何顺序。 |
TreeSet |
内部实现红黑树数据结构。与HashSet 一样,TreeSet 不允许存储重复项。然而,与HashSet 不同的是,它按照排序的顺序存储元素。它使用树数据结构来决定在哪里存储或搜索元素,位置由排序顺序决定。 |
HashMap |
在内部实现为哈希表数据结构。存储键和值对。使用散列法寻找一个位置来搜索或存储一对。搜索或插入速度非常快。它不以任何顺序存储元素。 |
TreeMap |
内部使用红黑树数据结构实现。与HashMap 不同,TreeMap 按照排序的顺序存储元素。它使用树数据结构来决定在哪里存储或搜索键,位置由排序顺序决定。 |
PriorityQueue |
使用堆数据结构在内部实现。一个PriorityQueue 用于基于优先级检索元素。无论插入的顺序如何,当删除元素时,将首先检索优先级最高的元素。 |
有许多旧的
java.util
类(现在称为遗留集合类型)被新的集合类所取代。其中有一些(括号内为较新的类型):Enumeration
(Iterator
)Vector
(ArrayList
)Dictionary
(Map
)Hashtable
(HashMap
)。另外,Stack
和Properties
是没有直接替换的遗留类。
数组列表类
用于存储一系列元素。您可以使用索引在特定位置插入容器的元素,并在以后检索相同的元素(即,它保持插入顺序)。您可以在列表中存储重复的元素。您需要知道两个具体的类:ArrayList
和LinkedList
。
ArrayList
实现一个可调整大小的数组。当您创建一个本机数组(比如说,new String[10];
)时,数组的大小在创建时就是已知的(固定的)。然而,ArrayList
是一个动态数组:它可以根据需要增加大小。在内部,ArrayList
分配一块内存,并根据需要增长。因此,在ArrayList
中访问数组元素非常快。但是,当您添加或删除元素时,会在内部复制其余的元素;因此添加/删除元素是一项成本很高的操作。
这里有一个简单的例子来访问ArrayList
中的元素。您使用一个ArrayList
并使用for-each
构造来遍历一个集合:
ArrayList<String> languageList = new ArrayList<>();
languageList.add("C");
languageList.add("C++");
languageList.add("Java");
for(String language : languageList) {
System.out.println(language);
}
它打印以下内容:
C
C++
Java
这个for-each
相当于下面的代码,它显式地使用了一个Iterator
:
for(Iterator<String> languageIter = languageList.iterator(); languageIter.hasNext();) {
String language = languageIter.next();
System.out.println(language);
}
该代码段也将打印与之前的for-each
循环代码相同的输出。下面是这个for
循环如何工作的逐步描述:
You use the iterator()
method to get the iterator for that container. Since languageList
is an ArrayList
of type <String>
, you should create Iterator
with String
. Name it languageIter
. Before entering the loop, you check if there are any elements to visit. You call the hasNext()
method for checking that. If it returns true, there are more elements to visit; if it returns false, the iteration is over and you exit the loop. Once you enter the body of the loop, the first thing you have to do is call next()
and move the iterator. The next()
method returns the iterated value. You capture that return value in the language
variable. You print the language
value, and then the loop continues.
这种迭代习惯——您调用iterator(), hasNext()
和next()
方法的方式——学习起来很重要;我们将在示例中广泛使用 for-each 循环或这种习惯用法。
注意,您创建了ArrayList<String>
和Iterator<String>
,而不是仅仅使用ArrayList
或Iterator
(也就是说,您提供了这些类的类型信息)。Collection
类是泛型类;因此,您需要指定类型参数来使用它们。这里你存储/迭代一个字符串列表,所以你使用<String>
。
使用迭代器遍历容器时,可以删除元素。让我们创建一个有十个元素的ArrayList<Integer>
类型的对象。您将遍历这些元素并删除它们(而不是使用ArrayList
中的removeAll()
方法)。清单 4-9 显示了代码。有用吗?
Listing 4-9. TestIterator.java
// This program shows the usage of Iterator
import java.util.ArrayList;
import java.util.Iterator;
class TestIterator {
public static void main(String []args) {
ArrayList<Integer> nums = new ArrayList<Integer>();
for(int i = 1; i < 10; i++)
nums.add(i);
System.out.println("Original list " + nums);
Iterator<Integer> numsIter = nums.iterator();
while(numsIter.hasNext()) {
numsIter.remove();
}
System.out.println("List after removing all elements" + nums);
}
}
它打印以下内容:
Original list [1, 2, 3, 4, 5, 6, 7, 8, 9]
Exception in thread "main" java.lang.IllegalStateException
at java.util.AbstractList$Itr.remove(AbstractList.java:356)
at TestIterator.main(Main.java:12)
哎呀!发生了什么事?问题是在调用remove()
之前,你还没有调用过next()
。在while
循环条件中检查hasNext()
,使用next()
移动到元素,并调用remove()
是移除元素的正确习惯用法。如果你没有正确地遵循它,你可能会陷入困境(即,你会得到IllegalStateException
)。类似地,如果您两次调用remove()
而没有在语句之间插入一个next()
,您将得到这个异常。
让我们通过在调用remove()
之前调用next()
来修复这个程序。以下是代码的相关部分:
Iterator<Integer> numsIter = nums.iterator();
while(numsIter.hasNext()) {
numsIter.next();
numsIter.remove();
}
System.out.println("List after removing all elements " + nums);
它打印出不含任何元素的列表,如预期的那样:
List after removing all elements []
记住
next()
需要在Iterator
中调用remove()
之前被调用;否则你会得到一个IllegalStateException
。类似地,在后续语句中调用remove()
而不在这些语句之间调用next()
也会导致这个异常。简而言之,当迭代器遍历容器时,对底层容器的任何修改都会导致这个异常。
使用 Arrays.asList()
java.util.Arrays
类有一个名为asList()
的有用方法,它返回一个固定大小的列表。关于返回的List
对象有一个有趣的方面:您不能添加或删除元素,但是您可以修改由asList()
方法返回的对象!同样,您通过List
所做的修改也反映在原始数组中(参见清单 4-10 )。
Listing 4-10. ArrayAsList.java
import java.util.List;
import java.util.Arrays;
class ArrayAsList {
public static void main(String []args) {
Double [] temperatureArray = {31.1, 30.0, 32.5, 34.9, 33.7, 27.8};
System.out.println("The original array is: " +
Arrays.toString(temperatureArray));
List<Double> temperatureList = Arrays.asList(temperatureArray);
temperatureList.set(0, 35.2);
System.out.println("The modified array is: " +
Arrays.toString(temperatureArray));
}
}
它打印以下内容:
The original array is: [31.1, 30.0, 32.5, 34.9, 33.7, 27.8]
The modified array is: [35.2, 30.0, 32.5, 34.9, 33.7, 27.8]
Arrays
类只提供有限的功能,你可能经常想使用Collections
类中的方法。为了实现这一点,调用Arrays.asList()
方法是一种有用的技术。
TreeSet 类
正如我们在高中数学课上所学的,没有重复的内容。与List
不同的是,Set
不记得你在哪里插入了元素(也就是说,它不记得插入顺序)。
Set
有两个重要的具体类:HashSet
和TreeSet
。HashSet
用于快速插入和检索元素;它不维护它所保存的元素的任何排序顺序。一个TreeSet
按照排序的顺序存储元素(它实现了SortedSet
接口)。
给定一个句子,你如何将句子中使用的字母按字母顺序排序?一个TreeSet
将这些值按顺序排列,所以你可以使用一个TreeSet
容器来解决这个问题(参见清单 4-11 )。
Listing 4-11. TreeSetTest.java
// This program demonstrates the usage of TreeSet class
import java.util.Set;
import java.util.TreeSet;
class TreeSetTest {
public static void main(String []args) {
String pangram = "the quick brown fox jumps over the lazy dog";
Set<Character> aToZee = new TreeSet<Character>();
for(char gram : pangram.toCharArray())
aToZee.add(gram);
System.out.println("The pangram is: " + pangram);
System.out.print("Sorted pangram characters are: " + aToZee);
}
}
它打印以下内容:
The pangram is: the quick brown fox jumps over the lazy dog
Sorted pangram characters are: [ , a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z]
盘符是一个至少使用一次字母表中所有字母的句子。你想在集合中存储一个盘符。因为您需要使用容器的引用类型,所以您已经创建了一个Character
的TreeSet
。
现在,如何从一个String
中获取字符?记住数组索引对String
s 无效。例如,为了获得第一个字符"t"
,如果你在程序中使用pangram[0]
,你将得到一个编译器错误。幸运的是,String
有一个名为toCharArray()
的方法返回一个char[]
。所以,你用这个方法遍历字符串,得到所有的字符。当您将字符添加到TreeSet
中时,字符会按排序顺序存储。所以,当你打印集合时,你会得到所有的小写字母。
请注意,在输出中有一个前导逗号。为什么呢?pangram 字符串有许多空白字符。一个空白也被存储在集合中,所以它也被打印出来!
地图界面
一个Map
存储键和值对。Map
接口没有扩展Collection
接口。然而,Map
接口中有一些方法可以让实现Collection
接口的对象类解决这个问题。还有,Map
中的方法名和Collection
中的方法非常相似,所以很容易理解和使用Map
。Map
有两个重要的具体类:HashMap
和TreeMap
。
- A
HashMap
内部使用哈希表数据结构。在HashMap
中,搜索(或查找元素)是一种快速操作。然而,HashMap
既不会记住您插入元素的顺序,也不会让元素保持任何排序的顺序。 - A
TreeMap
内部使用红黑树数据结构。与HashMap
不同的是,TreeMap
保持元素有序(即,按键排序)。因此,搜索或插入比HashMap
要慢一些。
NavigableMap 接口和 TreeMap 类
NavigableMap
接口扩展了SortedMap
接口。TreeMap
类是广泛使用的实现NavigableMap
的类。顾名思义,有了NavigableMap
,你就可以轻松操控Map
了。它有许多方法使导航变得容易。您可以获得与给定键匹配的最接近的值、小于给定键的所有值、大于给定键的所有值,以此类推。让我们看一个例子:列侬、麦卡特尼、哈里森和斯塔尔参加了在线考试。在那次考试中,他们的最高分是 100 分,及格分数是 40 分。如果想查找谁通过了考试等细节,并对考试成绩进行升序或降序排序,NavigableMap
(和TreeMap
)非常方便(参见清单 4-12 )。
Listing 4-12. NavigableMapTest.java
// This program demonstrates the usage of navigable tree interface and TreeMap class
import java.util.NavigableMap;
import java.util.TreeMap;
public class NavigableMapTest {
public static void main(String []args) {
NavigableMap<Integer, String> examScores = new TreeMap<Integer, String>();
examScores.put(90, "Sophia");
examScores.put(20, "Isabella");
examScores.put(10, "Emma");
examScores.put(50, "Olivea");
System.out.println("The data in the map is: " + examScores);
System.out.println("The data descending order is: " + examScores.descendingMap());
System.out.println("Details of those who passed the exam: " + examScores.tailMap(40));
System.out.println("The lowest mark is: " + examScores.firstEntry());
}
}
它打印以下内容:
The data in the map is: {10=Emma, 20=Isabella, 50=Olivea, 90=Sophia}
The data descending order is: {90=Sophia, 50=Olivea, 20=Isabella, 10=Emma}
Details of those who passed the exam: {50=Olivea, 90=Sophia}
The lowest mark is: 10=Emma
在这个程序中,你有一个NavigableMap<Integer, String>
来映射考试分数和人名。您创建一个TreeMap<Integer, String>
来实际存储考试分数。默认情况下,TreeMap
按升序存储数据。如果您希望数据按降序排列,这很容易:您只需使用descendingMap()
方法(或者如果您只对键感兴趣,使用descendingKeySet()
)。
假设及格分数为 40,您可能希望获得包含考试不及格人员数据的地图。为此,您可以使用键值为 40 的headMap()
方法(因为数据是按升序排列的,所以您希望从给定位置获取地图的“头部”部分)。同样,要得到通过考试的人的数据,可以使用tailMap()
的方法。
如果您想要高于和低于及格分数的直接分数,您可以分别使用higherEntry()
和lowerEntry()
方法。firstEntry()
和lastEntry()
方法给出了具有最低和最高键值的条目。所以,当你在examScores
上使用firstEntry()
方法时,你得到艾玛 10 分。如果你使用lastEntry()
,你会得到索菲亚,她有 90 分。
Deque 接口和 ArrayDeque 类
Deque
(双端队列)是一种数据结构,允许你从两端插入和删除元素。在 Java 6 的java.util.collection
包中引入了Deque
接口。Deque
接口扩展了Queue
接口。因此,Queue
提供的所有方法在Deque
接口中也是可用的。
Deque
接口的具体实现有三种:LinkedList
、ArrayDeque
和LinkedBlockingDeque
。让我们用ArrayDeque
来了解一下Deque
接口的特点。
考虑实现一个特殊的队列(比如说,支付水电费),在这个队列中,客户只能被添加到队列的末尾,并且可以从队列的前面(当客户支付账单时)或者从队列的末尾(当客户对长队感到沮丧并自己离开队列时)被删除。清单 4-13 展示了如何做到这一点。
Listing 4-13. SplQueueTest.java
// This program shows the usage of Deque interface
import java.util.ArrayDeque;
import java.util.Deque;
class SplQueue {
private Deque<String> splQ = new ArrayDeque<>();
void addInQueue(String customer){
splQ.addLast(customer);
}
void removeFront(){
splQ.removeFirst();
}
void removeBack(){
splQ.removeLast();
}
void printQueue(){
System.out.println("Special queue contains: " + splQ);
}
}
class SplQueueTest {
public static void main(String []args) {
SplQueue splQ = new SplQueue();
splQ.addInQueue("Harrison");
splQ.addInQueue("McCartney");
splQ.addInQueue("Starr");
splQ.addInQueue("Lennon");
splQ.printQueue();
splQ.removeFront();
splQ.removeBack();
splQ.printQueue();
}
}
它打印以下内容:
Special queue contains: [Harrison, McCartney, Starr, Lennon]
Special queue contains: [McCartney, Starr]
首先定义一个类——SplQueue
——它用基本的四个操作定义了一个类型为ArrayDeque
的容器splQ
。方法addInQueue()
在队列末尾添加一个客户,方法removeBack()
从队列末尾移除一个客户,方法removeFront()
从队列前面移除一个客户,方法printQueue()
简单地打印队列的所有元素。您只需使用来自Deque
接口的addLast(), removeFirst()
和removeLast()
方法来实现SplQueue
类的方法。在您的main()
方法中,您实例化了SplQueue
并调用了SplQueue
类的addInQueue()
方法。在这之后,您从前面移除一个客户,从末尾移除一个客户,并打印移除前后的队列内容。嗯,正如你所期望的那样。
ArrayList
和ArrayDeque
的区别在于,你可以使用索引在数组列表的任意位置添加元素;但是,只能在数组队列的前端或末端添加元素。这使得数组队列中的插入比数组列表更有效;然而,数组队列中的导航比数组列表中的导航开销更大。
可比和比较器接口
认证目标 |
---|
使用 java.util.Comparator 和 java.lang.Comparable 接口 |
顾名思义,Comparable
和Comparator
接口用于比较相似的对象(例如,在执行搜索或排序时)。假设你有一个容器,里面包含了一个Person
对象的列表。现在,你如何比较两个Person
物体?有许多可比较的属性,例如 SSN、姓名、驾照号码等等。可以在SSN
上比较两个物体,也可以在name
上比较一个人;这个要看上下文。因此,比较Person
对象的标准不能预先定义;开发人员必须定义这个标准。Java 定义了Comparable
和Comparator
接口来实现同样的功能。
Comparable
接口只有一个方法compareTo()
,声明如下:
int compareTo(Element that)
因为您在一个类中实现了compareTo()
方法,所以您有this
引用可用。您可以将the current element
与传递的Element
进行比较,并返回一个int
值。int
值应该是多少?下面是返回整数值的规则:
return 1 if current object > passed object
return 0 if current object == passed object
return -1 if current object < passed object
现在,一个重要的问题:现在>,< or == mean for an 【 ? Hmm, it is left to you to decide how to compare two objects! But the meaning of comparison should be a natural one; in other words, the comparison should mean natural ordering. For example, you saw how 【 s are compared with each other, based on a numeric order, which is the natural order for 【 types. Similarly, you compare 【 s using lexicographic comparison, which is the natural order for 【 s. For user-defined classes, you need to find the natural order in which you can compare the objects. For example, for a 【 class, 【 might be the natural order for comparing 【 objects. Listing 4-14 实现了一个简单的Student
类。
Listing 4-14. ComparatorTest1.java
// This program shows the usage of Comparable interface
import java.util.Arrays;
class Student implements Comparable<Student> {
String id;
String name;
Double cgpa;
public Student(String studentId, String studentName, double studentCGPA) {
id = studentId;
name = studentName;
cgpa = studentCGPA;
}
public String toString() {
return " \n " + id + " \t " + name + " \t " + cgpa;
}
public int compareTo(Student that) {
return this.id.compareTo(that.id);
}
}
class ComparatorTest1 {
public static void main(String []args) {
Student []students = { new Student("cs011", "Lennon ", 3.1),
new Student("cs021", "McCartney", 3.4),
new Student("cs012", "Harrison ", 2.7),
new Student("cs022", "Starr ", 3.7) };
System.out.println("Before sorting by student ID");
System.out.println("Student-ID \t Name \t CGPA (for 4.0) ");
System.out.println(Arrays.toString(students));
Arrays.sort(students);
System.out.println("After sorting by student ID");
System.out.println("Student-ID \t Name \t CGPA (for 4.0) ");
System.out.println(Arrays.toString(students));
}
}
它打印以下内容:
Before sorting by student ID
Student-ID Name CGPA (for 4.0)
[
cs011 Lennon 3.1,
cs021 McCartney 3.4,
cs012 Harrison 2.7,
cs022 Starr 3.7]
After sorting by student ID
Student-ID Name CGPA (for 4.0)
[
cs011 Lennon 3.1,
cs012 Harrison 2.7,
cs021 McCartney 3.4,
cs022 Starr 3.7]
您已经实现了Comparable<Student>
接口。当您调用sort()
方法时,它会调用compareTo()
方法来根据ID
比较Student
对象。因为Student ID
是唯一的,所以这是一种自然的比较顺序,效果很好。
现在,您可能需要根据学生获得的累计平均绩点(CGPA)来安排学生。你甚至可能需要根据它们的name
来比较Student
,如果你需要实现两个或更多的方法来比较两个相似的对象,那么你可以实现Comparator interface
。清单 4-15 是一个实现(在Student
类中没有变化,所以我们在这里不再生产它)。
Listing 4-15. ComparatorTest2.java
// This program shows the implementation of Comparator interface
import java.util.Arrays;
import java.util.Comparator;
class CGPAComparator implements Comparator<Student> {
public int compare(Student s1, Student s2) {
return (s1.cgpa.compareTo(s2.cgpa));
}
}
class ComparatorTest2 {
public static void main(String []args) {
Student []students = { new Student("cs011", "Lennon ", 3.1),
new Student("cs021", "McCartney", 3.4),
new Student("cs012", "Harrison ", 2.7),
new Student("cs022", "Starr ", 3.7) };
System.out.println("Before sorting by CGPA ");
System.out.println("Student-ID \t Name \t CGPA (for 4.0) ");
System.out.println(Arrays.toString(students));
Arrays.sort(students, new CGPAComparator());
System.out.println("After sorting by CGPA");
System.out.println("Student-ID \t Name \t CGPA (for 4.0) ");
System.out.println(Arrays.toString(students));
}
}
它打印以下内容:
Before sorting by CGPA
Student-ID Name CGPA (for 4.0)
[
cs011 Lennon 3.1,
cs021 McCartney 3.4,
cs012 Harrison 2.7,
cs022 Starr 3.7]
After sorting by CGPA
Student-ID Name CGPA (for 4.0)
[
cs012 Harrison 2.7,
cs011 Lennon 3.1,
cs021 McCartney 3.4,
cs022 Starr 3.7]
是的,程序打印按 CGPA 排序的Student
数据。你没改Student
类;该类仍然实现了Comparable<String>
接口并定义了compareTo()
方法,但是您不需要在程序中使用compareTo()
方法。您创建了一个名为CGPAComparator
的独立类,并实现了Comparator<Student>
接口。您定义了compare()
方法,它将两个Student
对象作为参数。通过使用来自Double
类的compareTo()
方法来比较参数s1
和s2
的 CGPA。除了调用sort()
方法的方式之外,您没有改变main()
方法中的任何东西。您创建一个新的CGPAComparator()
对象,并作为第二个参数传递给sort()
方法。默认情况下sort()
使用compareTo()
方法;因为您是显式传递一个Comparator
对象,所以它现在使用在CGPAComparator
中定义的compare()
方法。因此,Student
对象现在根据它们的 CGPA 进行比较和排序。
大多数类都有比较对象的自然顺序,所以在那些情况下使用
Comparable
接口。如果你想比较自然顺序之外的对象,或者如果你的类类型没有自然顺序,那么使用Comparator
接口。
收集流和过滤器
认证目标 |
---|
集合流和过滤器 |
Java 8 中引入的java.util.stream
包中提供了新的流 API。这个包中的主要类型是Stream<T>
接口,它是对象引用的流。IntStream
、LongStream
、DoubleStream
分别是原始类型int
、long
和double
的流。
Java 8 中的Co
llection
接口增加了stream()
和parallelStream()
方法。流是一系列元素。使用stream()
方法获得流时,可以执行顺序操作,使用parallelStream()
方法可以执行并行操作。(我们将在第十一章中讨论并行流。)由于List
、Set
、Deque
、Queue
等接口扩展了Collection
接口,所以可以从实现这些接口的集合类中获得流或并行流。例如,您可以从一个ArrayList
对象获得一个流。
流提供了管道功能——您可以过滤、映射和搜索数据。换句话说,流操作可以“链接”在一起,形成一个管道,称为“流管道”。我们将在本节稍后介绍流管道,并在专门讨论流的章节中详细介绍它们(Java Stream API 的第六章)。
流最常见的来源是集合对象,如集合、映射和列表。但是,请注意,我们可以独立于集合使用 streams API。在本章的其余部分,我们将讨论集合如何与流一起使用。
使用 forEach 迭代
认证目标 |
---|
使用流和列表的 forEach 方法进行迭代 |
作为 Java 程序员,我们习惯于对集合执行外部迭代。例如,考虑以下字符串列表:
List<String> strings = Arrays.asList("eeny", "meeny", "miny", "mo");
当我们使用 for 循环遍历这样一个集合时,我们使用的是外部迭代,如:
for(String string : strings) {
System.out.println(string);
}
内部迭代将迭代留给库代码。同样的代码可以转换成下面的等价代码,这些代码使用了 lambda 表达式(清单 4-16 ):
Listing 4-16. InternalIteration.java
import java.util.Arrays;
import java.util.List;
public class InternalIteration {
public static void main(String []args) {
List<String> strings = Arrays.asList("eeny", "meeny", "miny", "mo");
strings.forEach(string -> System.out.println(string));
}
}
该程序打印:
eeny
meeny
miny
mo
注意,List
接口扩展了Iterable
接口,后者有一个默认的forEach
方法(这个方法是在 Java 8 中添加的)。因此,我们能够通过调用strings
对象上的forEach
方法来执行内部迭代,并将一个 lambda 表达式作为参数传递给它。
虽然这个例子很简单,但是它说明了 Java 8 方法的一个主要变化:我们正在从外部迭代转移到内部迭代。事实上,整个 Stream API ( 第六章)都是基于内部迭代的概念。
在我们讨论Stream
接口和流管道之前,让我们讨论一个与 lambda 函数相关的重要话题:方法引用。
流的方法引用
认证目标 |
---|
对流使用方法引用 |
在清单 4-16 中,我们使用了这个 lambda 表达式:
strings.forEach(string -> System.out.println(string));
这段代码有些冗长,因为我们采用了string
参数并将其传递给了System.out.println
。幸运的是,Java 8 引入了一个称为“方法引用”的特性。方法引用使用“::”运算符。下面是一个使用方法引用的简化表达式:
strings.forEach(System.out::println);
方法引用路由给定的参数。在这种情况下,System.out::println
相当于使用 lambda 表达式string -> System.out.println(string)
。
把下面的语句简化成使用方法引用怎么样?
strings.forEach(string -> System.out.println(string.toUpperCase()));
这段代码中的 lambda 表达式在给定的String
对象上调用toUpperCase()
方法。因为方法引用只是传递参数,所以您不能直接使用它们来简化这个 lambda 表达式。另一种方法是将这段代码放在一个方法中,并使用该方法的引用(清单 4-17 )。
Listing 4-17. MethodReference.java
import java.util.Arrays;
import java.util.List;
class MethodReference {
public static void printUpperCaseString(String string) {
System.out.println(string.toUpperCase());
}
public static void main(String []args) {
List<String> strings = Arrays.asList("eeny", "meeny", "miny", "mo");
strings.forEach(MethodReference::printUpperCaseString);
}
}
该程序打印:
EENY
MEENY
MINY
MO
在这种情况下,我们在MethodReference
类中引入了一个静态方法。printUpperCaseString
调用传递的String
参数上的toUpperCase()
方法,并打印结果字符串。
总而言之,方法引用有两个主要好处:
方法引用是传递参数的一种方式,因此使用它们通常比等效的 lambda 表达式更方便(代码更简洁)。例如,我们在清单 4-16 中看到了如何将System.out::println
用作arg -> System.out.println(arg)
的等价物。
方法引用语法使得使用方法作为 lambda 表达式变得更加容易(如清单 4-17 )。
了解流接口
认证目标 |
---|
描述流接口和流管道 |
Stream
接口是java.util.stream
包中提供的最重要的接口。IntStream
、LongStream
、DoubleStream
这三个班级分别是int
、long
和double
的Stream
专业。图 4-1 显示了这些流的继承层次。
图 4-1。
Some important interfaces in java.util.stream package
溪流管道
流操作可以“链”在一起,形成一个“流管道”。蒸汽管道由三部分组成(见图 4-2 ):
图 4-2。
The stream pipeline with examples
- Source:创建一个流(从一个集合或数组或使用
Stream
方法,如of()
和generate()
)。 - 中间操作:可以链接在一起的可选操作(如
Stream
界面中的map()
、filter()
、distinct()
和sorted()
方法)。 - 终端操作:产生一个结果(如
Stream
界面中的sum()
、collect()
、forEach()
、reduce()
方法)。
下面是一个流管道的例子(清单 4-18 )。
Listing 4-18. StreamPipelineExample.java
import java.util.Arrays;
class StreamPipelineExample {
public static void main(String []args) {
Arrays.stream(Object.class.getMethods()) // source
.map(method -> method.getName()) // intermediate op
.distinct() // intermediate op
.forEach(System.out::println); // terminal operation
}
}
这段代码打印出来
wait
equals
toString
hashCode
getClass
notify
notifyAll
Object.class.getMethods()
产生了一个由Object
类中的Method
对象组成的数组。操作map(method -> method.getName())
以数组的形式返回方法的名称(作为Stream
的一部分)。注意Object
类中的wait()
方法是一个重载方法。为了获得唯一的方法名,我们可以使用distinct()
操作来删除数组中的重复条目。最后,forEach()
终端操作打印出方法的名称。
理解流管道的一种方法是将管道的组件分解成单独的语句。清单 4-18 将零件分解成独立的组件,是清单 4-19 的等效代码。
Listing 4-19. StreamPipelineComponents.java
import java.util.Arrays;
import java.util.stream.Stream;
import java.lang.reflect.Method;
class StreamPipelineComponents {
public static void main(String []args) {
Method[] objectMethods = Object.class.getMethods();
Stream<Method> objectMethodStream = Arrays.stream(objectMethods);
Stream<String> objectMethodNames = objectMethodStream.map(method -> method.getName());
Stream<String> uniqueObjectMethodNames = objectMethodNames.distinct();
uniqueObjectMethodNames.forEach(System.out::println);
}
}
在这种情况下,我们通过对Object.class.getMethod()
的结果调用Arrays.stream()
方法来获得一个流——这是流的源。map()
和distinct()
方法都将流作为输入,并将(修改后的)流作为输出返回。最后,流上的forEach()
方法是管道中的终端操作。
不要将流中的
map
与java.util.Map
接口混淆。map()
方法是一种中间操作,它从传入流中获取元素,应用该操作,并生成元素流作为输出;Map
接口保存键值对。
溪流源头
流有许多来源,包括流接口、集合和数组中的生成器方法。让我们考虑一个简单的任务,获取一个 1 到 5 的整数值流。
You can use range
or iterate
factory methods in the IntStream
interface. IntStream.range(1, 6)
The range()
method takes two arguments: it starts from the start value (given as the first argument) and goes on adding 1 to result in stream elements till it reaches the end value (given as the second argument and is excluding that value itself). In this case, we have passed the values 1 and 6, so the reduce()
method generates the stream of integer values starting from 1, adds the value 1 and results in values 2, 3, 4, and 5, and stops there because it hit the end value 6. IntStream.iterate(1, i -> i + 1).limit(5)
The iterate()
method takes two arguments: the initial value (as the first argument) and iteratively calls the given function (as second argument) by using the initial value as the seed. In this case, the first argument is 1, and it iteratively calls i + 1
, generating the integer values 2, 3, 4, 5, … This is an infinite stream. We limit the stream to the first five values by calling limit(5)
over this infinite stream of integer values. You can use the stream()
method in java.util.Arrays
class to create a stream from a given array, as in: Arrays.stream(new int[] {1, 2, 3, 4, 5})
Arrays.stream(new Integer[] {1, 2, 3, 4, 5})
The stream()
method was added in the Arrays
class in Java 8: // in Arrays class
public static IntStream stream(int[] array) { /* returns a stream of integers */ }
public static <T> Stream<T> stream(T[] array) { /* returns a stream of T objects */ }
Overloaded versions of stream()
method takes long[]
, double[]
, and T[]
. Since we are passing an int[]
and the Integer[]
, the calls stream()
method resolve to stream(int [])
and stream(T[])
respectively and a integer stream is returned. We can also create streams using factories and builders. The of()
method is a factory method in the Stream
interface: Stream.of(1, 2, 3, 4, 5)
Stream.of(new Integer[]{1, 2, 3, 4, 5})
Stream
接口中重载的of()
方法接受变量参数列表或T
类型的元素。此外,您可以使用builder()
方法,通过添加每个元素来构建Stream
对象,如下所示:
Stream.builder().add(1).add(2).add(3).add(4).add(5).build()
这并不是您可以用来生成整数流的方法的详尽列表—这只是让您知道有许多方法可以获得流。如前所述,Collection
接口添加了方法stream()
和parallelStream()
。因此,任何Collection
对象都是流的源——您只需要对它调用stream()
或parallelStream()
方法。例如:
List<String> strings = Arrays.asList("eeny", "meeny", "miny", "mo");
strings.stream().forEach(string -> System.out.println(string));
在这种情况下,我们通过调用stream()
方法从List<String>
对象获取流。Java 库中还有许多返回流的其他类型,例如:
java.nio.file.Files
类中的lines()
方法java.util.regex.Pattern
类中的splitAsStream()
方法java.util.Random
类中的ints()
方法java.lang.String
类中的chars()
方法
这里有一些关于如何使用它们的简单快捷的方法。
The java.nio.file.Files
class has lines()
method that returns a Stream<String>
. This code prints the contents of the file “FileRead.java
” in the current directory. Files.lines(Paths.get("./FileRead.java")).forEach(System.out::println);
The java.util.Pattern
class has splitAsStream()
method that returns a Stream<String>
. This code splits the input string “java 8 streams” based on whitespace and hence prints the strings “java”, “8”, and “streams” on the console. Pattern.compile(" ").splitAsStream("java 8 streams").forEach(System.out::println);
The java.util.Random
class has ints()
method that returns an IntStream
. It generates an infinite stream of random integers; so to restrict the number of integers to 5 integers, we call limit(5)
on that stream. new Random().ints().limit(5).forEach(System.out::println);
The String
class has chars()
method (newly introduced in Java 8 in CharSequence
—an interface that String
class implements). This method returns an IntStream
(why IntStream
? Remember that there is no equivalent char
specialization for Streams
). This code calls sorted()
method on this stream, so the stream elements get sorted in ascending order. Because it is a stream of integers, this code uses "%c"
to explicitly force the conversion from int
to char
. "hello".chars().sorted().forEach(ch -> System.out.printf("%c ", ch));
// prints e h l l o
在这些例子中,我们已经使用了中间操作,如limit()
和sorted()
。现在让我们更详细地讨论这种中间操作。
中间操作
中间操作转换流中的元素。表 4-5 列出了Stream<T>
中一些重要的中间操作。我们将在 Streams API 的第六章的中讨论其他中间操作,如flatMap()
及其变体。
表 4-5。
Important Intermediate Operations in the Stream Interface
方法 | 简短描述 |
---|---|
Stream<T> filter(Predicate<? super T> check) |
删除check 谓词返回 false 的元素。 |
<R> Stream<R> map(Function<? super T,? extends R> transform) |
对流中的每个元素应用transform() 函数。 |
Stream<T> distinct() |
移除流中的重复元素;它使用equals() 方法来确定一个元素是否在流中重复。 |
Stream<T> sorted() Stream<T> sorted(Comparator<? super T> compare) |
按自然顺序对元素进行排序。重载版本需要一个Comparator ——你可以为此传递一个 lambda 函数。 |
Stream<T> peek(Consumer<? super T> consume) |
返回流中的相同元素,但也对元素执行传递的consume lambda 表达式。 |
Stream<T> limit(long size) |
如果流中的元素比给定的size 多,则删除元素。 |
注意,该表中的所有中间操作都返回一个Stream<T>
作为结果。
中间操作是可选的;在流管道中不需要任何中间操作。这里有一个简单的例子:
Stream.of(1, 2, 3, 4, 5).count();
这段代码返回值 5。在这种情况下,Stream.of()
方法是流源,count()
方法是终端操作。count()
方法返回流中元素的数量。
让我们在这个流管道中引入一个中间操作:
Stream.of(1, 2, 3, 4, 5).map(i -> i * i).count();
map()
操作将作为参数传递的给定函数应用于流的元素。在这种情况下,它对流中的元素求平方。这段代码还返回值 5。你能看到在这段代码中应用map()
方法的检查结果吗?你可以用peek()
的方法:
Stream.``of``(1, 2, 3, 4, 5).map(i -> i * i).peek(i -> System.``out
这段代码打印出来
1 4 9 16 25
这个例子还说明了如何将中间操作链接在一起。这之所以成为可能,是因为中间操作返回流。
现在,让我们在调用map()
方法之前添加一个peek()
方法,以了解它是如何工作的:
Stream.of(1, 2, 3, 4, 5)
.peek(i -> System.out.printf("%d ", i))
.map(i -> i * i)
.peek(i -> System.out.printf("%d ", i))
.count();
这段代码打印出来
1 1 2 4 3 9 4 16 5 25
从这个输出可以看出,流管道正在逐个处理元素。每个元素都映射到它的平方值。peek()
方法帮助我们理解流是如何处理元素的。
peek()
方法主要用于调试目的。它有助于我们理解元素在管道中是如何转换的。不要在产品代码中使用它。
过滤收藏
认证目标 |
---|
使用 lambda 表达式筛选集合 |
Stream
接口中的filter()
方法用于删除不符合给定条件的元素。这里有一个简单的例子,它使用了Stream
的filter()
方法来移除奇数整数(列表 4-20 )。
Listing 4-20. EvenNumbers.java
import java.util.stream.IntStream;
class EvenNumbers {
public static void main(String []args) {
IntStream.rangeClosed(0, 10)
.filter(i -> (i % 2) == 0)
.forEach(System.out::println);
}
}
这个程序打印
0
2
4
6
8
10
在这个例子中,我们使用了IntStream
类 Stream 对ints
的专门化之一。rangeClosed(startValue, endValueInclusiveOfEnd)
方法生成一个从startValue
到endValueInclusiveOfEnd
的整数序列。这里,rangeClosed(0, 10)
产生整数值 0,1,2,…,9,10(注意值 10)。还有一个类似的方法range(startValue, endValueExclusiveOfEnd)
,生成一个从startValue
开始直到(不包括)endValueExclusiveOfEnd
的整数序列。
根据这个rangeClosed()
方法的结果,我们对其应用filter()
方法。下面是filter()
的签名方法:
IntStream filter(IntPredicate predicate)
filter()
方法应用给定的谓词来确定该元素是应该作为返回流的一部分被包含还是被删除(即过滤)。java.util.function.IntPredicate
函数式接口具有如下签名的函数:
boolean test(int value);
这里我们传递一个 lambda 函数i -> (i % 2) == 0
来匹配返回一个boolean
值的IntPredicate
函数式接口。如果当前正在处理的元素返回 true(即,在这种情况下,它是偶数),那么它是流的一部分,或者它被消除。
或者,可以用IntPredicate
functional interface 的函数类型定义一个函数,并将其传递给 filter。
// you can define this static function within EvenNumbers class
public static boolean isEven(int i) {
return (i % 2) == 0;
}
现在,不用将 lambda 函数传递给filter()
方法,而是传递一个方法引用,如在filter(EvenNumbers::isEven)
中。
通常map()
和filter()
方法一起使用。例如,下面的程序打印偶数的平方(列出 4-21 )。
Listing 4-21. EvenSquares.java
import java.util.stream.IntStream;
class EvenSquares {
public static void main(String []args) {
IntStream.rangeClosed(0, 10)
.map(i -> i * i)
.filter(i -> (i % 2) == 0)
.forEach(System.out::println);
}
}
这个程序打印
0
4
16
36
64
100
但是,这段代码不必计算奇数的平方(奇数的平方总是奇数)。因此,我们可以改变map
和filter
操作的顺序,以消除那些不必要的计算:
IntStream.rangeClosed(0, 10)
.filter(i -> (i % 2) == 0)
.map(i -> i * i) // call map AFTER calling filter
.forEach(System.out::println);
这个输出是一样的。这个简单的例子展示了如何在不改变行为的情况下改变中间操作的顺序。
终端操作
您需要在管道的末端提供一个终端操作。这个终端操作通常会产生一个结果,比如在一个IntStream
上调用方法sum()
、min()
、max()
或average()
。终端操作也可以执行其他动作,比如用reduce()
、collect()
方法累加元素,或者只是执行一个动作,就像调用forEach()
方法一样。表 4-6 列出了Stream<T>
中一些重要的终端操作。
表 4-6。
Important Terminal Operations in the Stream Interface
方法 | 简短描述 |
---|---|
void forEach(Consumer<? super T> action) |
为流中的每个元素调用action 。 |
Object[] toArray() |
返回一个在流中有柠檬的Object 数组。 |
Optional<T> min(Comparator<? super T> compare) |
返回流中的最小值(使用给定的compare 函数比较对象)。 |
Optional<T> max(Comparator<? super T> compare) |
返回流中的最大值(使用给定的compare 函数比较对象)。 |
long count() |
返回流中元素的数量。 |
有许多重要的终端操作,如reduce()
、collect()
、findFirst()
、findAny()
、anyMatch()
、allMatch()
和noneMatch()
方法。我们将在后面关于流 API 的第六章的中讨论这些方法(以及本表中提到的Optional<T>
)。此外,IntStream
、LongStream
和DoubleStream
具有诸如sum()
、min()
、max()
和average()
的方法,分别对int
s、long
s 和double
s 的流进行操作。
下面是一个在Stream
接口中使用toArray()
方法的例子:
Object [] words = Pattern.``compile
System.``out``.println(Arrays.``stream``(words).mapToInt(str -> Integer.``valueOf
该程序打印:
15
在这个程序中,我们有一个字符串“1 2 3 4 5”并且splitAsStream()
返回一个流Strings
。我们已经将Strings
流转换成一个名为words
的Object
数组;然后,我们使用Arrays.stream(words)
将数组转换回流(只是为了说明如何将流转换成数组,然后再转换回来!).现在,我们将每个Object
条目映射到一个String
中,然后映射到一个整数值。最后,我们调用终端操作sum()
来获得整数之和为 15。
一旦一个终端操作完成,它所操作的流就被认为是“消耗的”。如果你试图再次“使用”这个流,你将得到一个IllegalStateException
(列表 4-22 )。
Listing 4-22. StreamReuse.java
import java.util.stream.IntStream;
public class StreamReuse {
public static void main(String []args) {
IntStream chars = "bookkeep".chars();
System.out.println(chars.count());
chars.distinct().sorted().forEach(ch -> System.out.printf("%c ", ch));
}
}
变量chars
指向从字符串“bookkeep”创建的流。当我们得到chars.count()
时,流就被“消耗”了。为什么?因为count()
方法是一种终端操作。因为我们试图在下一条语句中再次使用这个流,所以这个程序会因为抛出IllegalStateException
而崩溃。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
创建和使用泛型类
- 泛型将确保任何添加除指定类型之外的类型元素的尝试都会在编译时被捕获。因此,泛型提供了具有类型安全的泛型实现。
- Java 7 引入了菱形语法,其中类型参数(在新操作符和类名之后)可以省略。编译器将从类型声明中推断类型。
- 泛型不是协变的。也就是说,子类型对泛型不起作用;不能将派生的泛型类型参数赋给基类型参数。
- 避免混合原始类型和泛型类型。在其他情况下,请手动确保类型安全。
<?>
指定了泛型中的未知类型,被称为通配符。例如,List<?>
指的是一列未知数。
创建和使用 ArrayList、TreeSet、TreeMap 和 ArrayDeque 对象
- 术语集合、集合和集合是不同的。
Collection
—java.util.Collection<E>
—是集合层次结构中的根接口。Collections
—java.util.Collections
—是一个只包含静态方法的实用程序类。通用术语集合指的是像映射、堆栈和队列这样的容器。 - 请记住,您不能向由
Arrays.asList()
方法返回的List
添加或移除元素。但是,您可以对返回的List
中的元素进行更改,对该List
所做的更改会反映到数组中。 - 一个
HashSet
用于快速插入和检索元素;它不维护它所保存的元素的任何排序顺序。一个TreeSet
按照排序的顺序存储元素(它实现了SortedSet
接口)。 - A
HashMap
内部使用哈希表数据结构。在HashMap
中,搜索(或查找元素)是一种快速操作。然而,HashMap
既不会记住您插入元素的顺序,也不会让元素保持任何排序的顺序。与HashMap
不同的是,TreeMap
保持元素有序(即,按键排序)。因此,搜索或插入比HashMap
稍慢。 Deque
(双端队列)是一种允许你从两端插入和移除元素的数据结构。Deque
接口的具体实现有三种:LinkedList
、ArrayDeque
和LinkedBlockingDeque
。ArrayList
和ArrayDeque
的区别在于,你可以使用索引在数组列表中的任何地方添加元素;但是,只能在数组队列的前端或末端添加元素。
使用 java.util.Comparator 和 java.lang.Comparable 接口
- 在自然顺序可能的情况下,为您的类实现
Comparable
接口。如果您想比较自然顺序之外的对象,或者如果您的类类型没有自然顺序,那么创建实现Comparator
接口的单独的类。此外,如果你有多种选择来决定顺序,那么就使用Comparator
界面。
集合流和过滤器
- Java 8 中引入的
java.util.stream
包中提供了新的流 API。这个包中的主要类型是Stream<T>
接口,它是对象引用的流。IntStream
、LongStream
和DoubleStream
分别是原始类型int
、long
和double
的流。 - 流是一系列元素。在 Java 8 中,
Collection
接口增加了stream()
和parallelStream()
方法,从这两个方法中可以分别获得顺序流和并行流。
使用流和列表的 forEach 方法进行迭代
- 在 Java 8 中,我们正从外部迭代转向内部迭代。这是 Java 8 函数式编程方法的一个重大变化。
- 接口
Stream
和Iterable
定义了forEach()
方法。forEach()
方法支持内部迭代。
描述流接口和流管道
- 流操作可以“链接”在一起,形成一个称为“流管道”的管道。
- 流管道有开始、中间和结束:源(创建流)、中间操作(由可以链接在一起的可选操作组成)和终端操作(产生结果)。
- 终端操作可以产生一个结果,累积流元素,或者只是执行一个动作。
- 一个流只能使用一次。任何重用流的尝试(例如,通过调用中间或终端操作)都将导致抛出一个
IllegalStateException
。
使用 lambda 表达式筛选集合
Stream
接口中的filter()
方法用于删除不符合给定条件的元素。
对流使用方法引用
- 当 lambda 表达式只是路由给定的参数时,您可以使用方法引用来代替。
- 因为方法引用是传递参数的一种方式,所以使用它们通常比使用它们的等效 lambda 表达式更方便(因为这会产生更简洁的代码)。
Question TimeChoose the correct option based on this program: import java.util.*;
class UtilitiesTest {
public static void main(String []args) {
List<int> intList = new ArrayList<>();
intList.add(10);
intList.add(20);
System.out.println("The list is: " + intList);
}
}
It prints the following: The list is: [10, 20]
It prints the following: The list is: [20, 10]
It results in a compiler error It results in a runtime exception Choose the correct option based on this program: import java.util.*;
class UtilitiesTest {
public static void main(String []args) {
List<Integer> intList = new LinkedList<>();
List<Double> dblList = new LinkedList<>();
System.out.println("First type: " + intList.getClass());
System.out.println("Second type:" + dblList.getClass());
}
}
It prints the following: First type: class java.util.LinkedList Second type:class java.util.LinkedList It prints the following: First type: class java.util.LinkedListimport java.util.Arrays;
class DefaultSorter {
public static void main(String[] args) {
String[] brics = {"Brazil", "Russia", "India", "China"};
Arrays.sort(brics, null); // LINE A
for(String country : brics) {
System.out.print(country + " ");
}
}
}
This program will result in a compiler error in line marked with comment LINE A When executed, the program prints the following: Brazil Russia India China When executed, the program prints the following: Brazil China India Russia When executed, the program prints the following: Russia India China Brazil When executed, the program throws a runtime exception of NullPointerException
when executing the line marked with comment LINE A When executed, the program throws a runtime exception of InvalidComparatorException
when executing the line marked with comment LINE A Choose the correct option based on this code segment: "abracadabra".chars().distinct().peek(ch -> System.
out
.printf("%c ", ch)).sorted();
It prints: “a b c d r” It prints: “a b r c d” It crashes by throwing a java.util.IllegalFormatConversionException
This program terminates normally without printing any output in the console Choose the correct option based on this code segment: IntStream.rangeClosed(1, 1).forEach(System.out::println);
It prints: 1 It crashes by throwing a java.lang.UnsupportedOperationException
It crashes by throwing a java.lang.StackOverflowError
It crashes by throwing a java.lang.IllegalArgumentException
This program terminates normally without printing any output in the console Choose the correct option based on this program: import java.util.stream.DoubleStream;
public class DoubleUse {
public static void main(String []args) {
DoubleStream nums = DoubleStream.
of
(1.0, 2.0, 3.0).map(i -> -i); // #1
System.
out
.printf("count = %d, sum = %f", nums.count(), nums.sum());
}
}
This program results in a compiler error in the line marked with comment #1 This program prints: "count = 3, sum = -6.000000"
This program crashes by throwing a java.util.IllegalFormatConversionException
This program crashes by throwing a java.lang.IllegalStateException
Choose the correct option based on this program: class Consonants {
private static boolean removeVowels(int c) {
switch(c) {
case 'a': case 'e': case 'i': case 'o': case 'u': return true;
}
return false;
}
public static void main(String []args) {
"avada kedavra".chars()
.filter(Consonants::removeVovels)
.forEach(ch -> System.out.printf("%c", ch));
}
}
This program results in a compiler error This program prints: "aaaeaa"
This program prints: "vd kdvr"
This program prints: "
avada kedavra"
This program crashes by throwing a java.util.IllegalFormatConversionException
This program crashes by throwing a java.lang.IllegalStateException
Choose the correct option based on this program: import java.util.*;
class DequeTest {
public static void main(String []args) {
Deque<Integer> deque = new ArrayDeque<>();
deque.addAll(Arrays.asList(1, 2, 3, 4, 5));
System.out.println("The removed element is: " + deque.remove()); // ERROR?
}
}
When executed, this program prints the following: “The removed element is: 5” When executed, this program prints the following: “The removed element is: 1” When compiled, the program results in a compiler error of “remove() returns void
” for the line marked with the comment ERROR
. When executed, this program throws InvalidOperationException
. Determine the behavior of this program: import java.io.*;
class LastError<T> {
private T lastError;
void setError(T t){
lastError = t;
System.out.println("LastError: setError");
}
}
class StrLastError<S extends CharSequence> extends LastError<String>{
public StrLastError(S s) {
}
void setError(S s){
System.out.println("StrLastError: setError");
}
}
class Test {
public static void main(String []args) {
StrLastError<String> err = new StrLastError<String>("Error");
err.setError("Last error");
}
}
It prints the following: StrLastError: setError
It prints the following: LastError: setError
It results in a compilation error It results in a runtime exception
答案:
C. It results in a compiler error You cannot specify primitive types along with generics, so List<int>
needs to be changed to List<Integer>
. A. It prints the following: First type: class java.util.LinkedList Second type:class java.util.LinkedList Due to type erasure, after compilation both types are treated as same LinkedList
type. C. When executed, the program prints the following: Brazil China India Russia When null
is passed as a second argument to the Arrays.sort()
method, it means that the default Comparable
(i.e., natural ordering for the elements) should be used. The default Comparator
results in sorting the elements in ascending order. The program does not result in a NullPointerException
or any other exceptions or a compiler error. D. This program terminates normally without printing any output in the console A stream pipeline is lazily evaluated. Since there is no terminal operation provided (such as count
, forEach
, reduce,
or collect
), this pipeline is not evaluated and hence the peek does not print any output to the console. A. It prints: 1 The rangeClosed(startValue, endValueInclusiveOfEnd)
method generates a sequence of integers starting with startValue
till (and inclusive of) endValueInclusiveOfEnd
. Hence the call IntStream.rangeClosed(1, 1)
results in a stream with only one element and the forEach() method prints that value. D. This program crashes by throwing a java.lang.IllegalStateException
A stream is considered “consumed” when a terminal operation is called on that stream. The methods count()
and sum()
are terminal operations in DoubleStream
. When this code calls nums.count()
, the underlying stream is already “consumed”. When the printf
calls nums.sum()
, this program results in throwing java.lang.IllegalStateException
due to the attempt to use a consumed stream. B. This program prints: "aaaeaa"
Because the Consonants::removeVowels
returns true when there is a vowel passed, only those characters are retained in the stream by the filter
method. Hence, this program prints “aaaeaa”. B. When executed, this program prints the following: “The removed element is: 1”. The remove()
method is equivalent to the removeFirst()
method, which removes the first element (head of the queue) of the Deque
object. C. It results in a compilation error It looks like the setError()
method in StrLastError
is overriding setError()
in the LastError
class. However, it is not the case. At the time of compilation, the knowledge of type S
is not available. Therefore, the compiler records the signatures of these two methods as setError(String)
in superclass and setError(S_extends_CharSequence)
in subclass—treating them as overloaded methods (not overridden). In this case, when the call to setError()
is found, the compiler finds both the overloaded methods matching, resulting in the ambiguous method call error. Here is the error message Test.java:22: error: reference to setError is ambiguous, both method setError(T) in LastError and method setError(S) in StrLastError match
err.setError("Last error");
^
where T and S are type-variables: T extends Object
declared in class LastError
. S extends CharSequence
declared in class StrLastError
.
五、Lambda 内置函数式接口
认证目标 |
---|
使用 java.util.function 包中包含的内置接口,如谓词、使用者、函数和供应商 |
开发使用函数式接口原始版本的代码 |
开发使用函数式接口二进制版本的代码 |
开发使用一元运算符接口的代码 |
java.util.function
有许多内置接口。Java 库中的其他包(特别是java.util.stream
包)使用这个包中定义的接口。对于 OCPJP 8 考试,你应该熟悉使用本软件包中提供的关键接口。
正如我们之前讨论的(在第三章中),一个函数式接口声明了一个抽象方法(但是除此之外,它可以有任意数量的默认或静态方法)。函数式接口对于创建 lambda 表达式很有用。整个java.util.function
包由功能接口组成。
在定义您自己的功能接口之前,请根据您的需要考虑使用
java.util.function
包中定义的现成的功能接口。如果您正在寻找的 lambda 函数的签名在该库中提供的任何函数式接口中都不可用,您可以定义自己的函数式接口。
使用内置的功能接口
认证目标 |
---|
使用 java.util.function 包中包含的内置接口,如谓词、使用者、函数和供应商 |
在本节中,让我们讨论一下java.util.function
包中包含的四个重要的内置接口:Predicate
、Consumer
、Function
和Supplier
。参见表 5-1 和图 5-1 获得这些功能接口的概述。
图 5-1。
Abstract method declarations in key functional interfaces in java.util.function package
表 5-1。
Key Functional Interfaces in java.util.function Package
功能接口 | 简要描述 | 多畜共牧 |
---|---|---|
Predicate<T> |
检查条件并返回一个布尔值作为结果 | 在java.util.stream.Stream 中的filter() 方法中,用于删除流中与给定条件(即谓词)不匹配的元素作为参数。 |
Consumer<T> |
接受参数但不返回任何值的操作 | 在集合中的forEach() 方法和在java.util.stream.Stream 中;此方法用于遍历集合或流中的所有元素。 |
Function<T, R> |
接受参数并返回结果的函数 | 在java.util.stream.Stream 的map() 方法中,对传递的值进行转换或操作,并返回结果。 |
Supplier<T> |
向调用者返回值的操作(返回值可以是相同或不同的值) | 在java.util.stream.Stream 中的generate() 方法创建一个无限元素流。 |
谓词接口
在代码中,我们经常需要使用检查条件并返回布尔值的函数。考虑下面的代码段:
Stream.of("hello", "world")
.filter(str -> str.startsWith("h"))
.forEach(System.out::println);
这段代码只是在控制台上打印“hello”。只有当传递的字符串以“h”开头时,filter()
方法才返回 true,因此它从流中“过滤掉”字符串“world ”,因为该字符串不是以“h”开头的。在这段代码中,filter()
方法将一个Predicate
作为参数。这里是Predicate
功能界面:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
// other methods elided
}
名为test()
的抽象方法接受一个参数并返回true
或false
(图 5-2 )。
图 5-2。
A Predicate
A
Predicate<T>
“确认”某事为true
或false
:它接受一个T
类型的参数,并返回一个boolean
值。你可以在一个Predicate
对象上调用test()
方法。
这个函数式接口还定义了名为and()
和or()
的默认方法,它们接受一个Predicate
并返回一个Predicate
。这些方法的行为类似于& &和||操作符。方法negate()
返回一个Predicate
,它的行为类似于!操作员。它们有什么用?这里有一个程序说明了在Predicate
接口中and()
方法的使用(清单 5-1 )。
Listing 5-1. PredicateTest.java
import java.util.function.Predicate;
public class PredicateTest {
public static void main(String []args) {
Predicate<String> nullCheck = arg -> arg != null;
Predicate<String> emptyCheck = arg -> arg.length() > 0;
Predicate<String> nullAndEmptyCheck = nullCheck.and(emptyCheck);
String helloStr = "hello";
System.out.println(nullAndEmptyCheck.test(helloStr));
String nullStr = null;
System.out.println(nullAndEmptyCheck.test(nullStr));
}
}
该程序打印:
true
false
在这个程序中,对象nullCheck
是一个Predicate
,如果给定的String
参数不是null
,它将返回true
。如果给定的字符串不为空,emptyCheck
谓词返回 true。nullAndEmptyCheck
谓词通过使用Predicate
中提供的名为and()
的默认方法来组合nullCheck
和emptyCheck
谓词。由于helloStr
在第一次调用nullAndEmptyCheck.test(helloStr)
中指向字符串“hello”,且该字符串不为空,所以返回true
。然而,在下一个调用中,nullStr
为空,因此调用nullAndEmptyCheck.test(nullStr)
返回false
。
再举一个使用Predicate
s 的例子,这里有一段代码使用了 Java 8 的Collection
接口中添加的removeIf()
方法(清单 5-2 )。
Listing 5-2. RemoveIfMethod.java
import java.util.List;
import java.util.ArrayList;
public class RemoveIfMethod {
public static void main(String []args) {
List<String> greeting = new ArrayList<>();
greeting.add("hello");
greeting.add("world");
greeting.removeIf(str -> !str.startsWith("h"));
greeting.forEach(System.out::println);
}
}
它在控制台中打印“hello”。在Collection
接口(ArrayList
的超级接口)中定义的默认方法removeIf()
以一个Predicate
作为参数:
default boolean removeIf(Predicate<? super E> filter)
在对removeIf()
方法的调用中,我们传递了一个 lambda 表达式,它与在Predicate
接口中声明的抽象方法boolean test(T t)
相匹配:
greeting.removeIf(str -> !str.startsWith("h"));
结果,ArrayList
对象问候语中的字符串“world”被删除,因此控制台中只显示“hello”。在这段代码中,我们使用了!操作员。除此之外,使用在Predicate
中定义的等价的negate()
方法怎么样?是的,这是可能的,下面是更改后的代码:
greeting.removeIf(((Predicate<String>) str -> str.startsWith("h")).negate());
当您执行清单 5-2 中的程序时,程序会打印“hello”。注意我们是如何在这个表达式中执行显式类型转换(
到Predicate<String>)
的。没有这种显式的类型转换——就像在((str -> str.startsWith("h")).negate())
中一样——编译器不能执行类型推断来确定匹配的函数式接口,因此会报告一个错误。
消费者界面
有许多方法接受一个参数,根据该参数执行一些操作,但不向它们的调用者返回任何东西——它们是消费者方法。考虑下面的代码段:
Stream.of("hello", "world")
.forEach(System.out::println);
这段代码通过使用在Stream
接口中定义的forEach()
方法打印单词“hello”和“world ”,它们是流的一部分。该方法在java.util.stream.Stream
界面中声明如下:
void forEach(Consumer<? super T> action);
forEach()
将Consumer
的一个实例作为参数。Consumer
函数式接口声明了一个名为accept()
的抽象方法(图 5-3 ):
图 5-3。
A Consumer
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
// the default andThen method elided
}
accept()
方法“消费”一个对象,不返回任何东西(void
)。
A
Consumer<T>
“消费”某物:它接受一个参数(泛型类型T
)并且不返回任何东西(void
)。你可以在一个Consumer
对象上调用accept()
方法。
下面是一个使用Consumer
接口的例子:
Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
printUpperCase.accept("hello");
// prints: HELLO
在这段代码中,lambda 表达式接受给定的字符串,转换成大写字母,并将其打印到控制台。我们将实际的参数“hello”传递给accept()
方法。
现在,让我们回到对forEach()
的讨论:调用forEach(System.out::println)
是如何工作的?
System
类有一个名为out
的静态变量,它的类型是PrintStream
。PrintStream
类定义了重载的println
方法;其中一个重载方法有签名void println(String)
。在调用forEach(System.out::println)
中,我们传递的是println
的方法引用,即System.out::println
。该方法引用匹配Consumer
接口中抽象方法的签名,即void accept(T)
。因此,方法引用System.out::println
用于实现函数式接口Consumer
,代码将字符串“hello”和“world”打印到控制台。清单 5-3 将代码Stream.of("hello", "world").forEach(System.out::println);
分成三个不同的语句,只是为了展示它是如何工作的。
Listing 5-3. ConsumerUse.java
import java.util.stream.Stream;
import java.util.function.Consumer;
class ConsumerUse {
public static void main(String []args) {
Stream<String> strings = Stream.of("hello", "world");
Consumer<String> printString = System.out::println;
strings.forEach(printString);
}
}
该程序打印:
hello
world
Consumer
也有一个名为andThen()
的默认方法;它允许链接对Consumer
对象的调用。
功能界面
考虑这个在java.util.stream.Stream
接口中使用map()
方法的例子(列表 5-4 ):
Listing 5-4. FunctionUse.java
import java.util.Arrays;
public class FunctionUse {
public static void main(String []args) {
Arrays.stream("4, -9, 16".split(", "))
.map(Integer::parseInt)
.map(i -> (i < 0) ? -i : i)
.forEach(System.out::println);
}
}
该程序打印:
4
9
16
这个程序通过拆分字符串“4,-9,16”来创建一个String
流。方法引用Integer::parseInt
被传递给map()
方法——这个调用为流中的每个元素返回一个Integer
对象。在流中对map()
方法的第二次调用中,我们使用了 lambda 函数(i -> (i < 0) ? -i : i)
来产生一个非负整数列表(或者,我们可以使用Math::abs
方法)。我们在这里使用的map()
方法将一个Function
作为参数(这个例子是为了说明Function
接口的用处)。最后,使用forEach()
方法打印结果整数。
Function
接口定义了一个名为apply()
的抽象方法,它接受一个泛型类型T
的参数并返回一个泛型类型R
的对象(图 5-4 ):
图 5-4。
A Function<T, R> takes an argument of type T and returns a value of type R
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// other methods elided
}
Function
接口也有默认的方法,比如compose(), andThen()
和identity()
。
A
Function<T, R>
“操作”某物并返回某物:它接受一个参数(泛型类型T
)并返回一个对象(泛型类型R
)。你可以在一个Function
对象上调用apply()
方法。
下面是一个使用Function
的简单例子:
Function<String, Integer> strLength = str -> str.length();
System.out.println(strLength.apply("supercalifragilisticexpialidocious"));
// prints: 34
这段代码获取一个字符串并返回它的长度。对于调用strLength.apply
,我们传递字符串“supercalifragilisticexpialidocious"
。作为调用apply()
的结果,我们得到字符串 34 的长度作为结果。
让我们将清单 5-4 中的早期程序改为使用andThen()
方法(清单 5-5 )。
Listing 5-5. CombineFunctions.java
import java.util.Arrays;
import java.util.function.Function;
public class CombineFunctions {
public static void main(String []args) {
Function<String, Integer> parseInt = Integer::``parseInt
Function<Integer, Integer> absInt = Math::``abs
Function<String, Integer> parseAndAbsInt = parseInt.andThen(absInt);
Arrays.``stream
.map(parseAndAbsInt)
.forEach(System.``out
}
}
该程序在单独的行中打印 4、9 和 16:与清单 5-4 的输出相同,但是在Stream
中对map()
方法进行了一次调用。因为Integer::parseInt()
将一个字符串作为参数,解析它以返回一个Integer
,我们声明了Function<String, Integer>
类型的parseInt()
方法。Math::abs
方法接受一个整数并返回一个整数,因此我们将其声明为类型Function<Integer, Integer>
。因为parseAndAbsInt
接受一个String
作为参数并返回一个整数作为结果,我们声明它的类型为Function<String, Integer>
。
Function
界面中的andThen()
和compose()
方法有什么区别?andThen()
方法在调用当前的Function
后应用传递的参数(如本例所示)。compose()
方法在调用当前Function
之前调用参数,如:
Function<String, Integer> parseAndAbsInt = absInt.compose(parseInt);
Function
中的identity()
函数只是返回传递的参数,不做任何事情!那它有什么用?它有时用于测试——当你写了一段使用了Function
的代码并想检查它是否工作时,你可以调用identity()
,因为它不做任何事情。这里有一个例子:
Arrays.stream("4, -9, 16".split(", "))
.map(Function.identity())
.forEach(System.out::println);
在这段代码中,map(Function.identity())
什么也不做;它只是将流中的元素传递给调用forEach(System.out::println)
。因此,代码按原样打印元素,即值 4、-9 和 16 在单独的行中。
供应商界面
在程序中,我们经常需要使用一种不接受任何输入但返回一些输出的方法。考虑以下生成Boolean
值的程序(列表 5-6 ):
Listing 5-6. GenerateBooleans.java
import java.util.stream.Stream;
import java.util.Random;
class GenerateBooleans {
public static void main(String []args) {
Random random = new Random();
Stream.generate(random::nextBoolean)
.limit(2)
.forEach(System.out::println);
}
}
这个程序随机打印两个布尔值,例如,“真”和“假”。Stream
接口中的generate()
方法是一个静态成员,以一个Supplier
作为参数;
static <T> Stream<T> generate(Supplier<T> s)
这里,您正在传递在java.util.Random
类中定义的nextBoolean
的方法引用。它返回一个随机选择的布尔值:
boolean nextBoolean()
您可以将nextBoolean
的方法引用传递给Stream
的generate()
方法,因为它匹配Supplier
接口中的抽象方法,即T get()
图 5-5 。
图 5-5。
A Supplier
@FunctionalInterface
public interface Supplier<T> {
T get();
// no other methods in this interface
}
A
Supplier<T>
“supplies”什么都不带,只返回一些东西:它没有参数,返回一个对象(通用类型T
)。你可以在一个Supplier
对象上调用get()
方法。
下面是一个简单的例子,它不需要任何输入就可以返回值:
Supplier<String> currentDateTime = () -> LocalDateTime.now().toString();
System.out.println(currentDateTime.get());
我们已经在java.time.LocalDateTime
上调用了now()
方法(我们将在第八章中讨论 java 日期和时间 API)。当我们执行它时,它打印:2015-10-16T12:40:55.164
。当然,如果您尝试这段代码,您将得到不同的输出。这里我们用的是Supplier<String>
。lambda 表达式不接受任何输出,而是以String
格式返回当前日期/时间。当我们在currentDateTime
变量上调用get()
方法时,我们正在调用 lambda。
构造函数引用
考虑以下代码:
Supplier<String> newString = String::new;
System.out.println(newString.get());
// prints an empty string (nothing) to the console and then a newline character
这段代码使用了构造函数引用。此代码相当于:
Supplier<String> newString = () -> new String();
System.out.println(newString.get());
通过使用::new
的方法引用,这个 lambda 表达式得到了简化,如String::new
所示。如何使用带参数的构造函数?例如,考虑构造函数Integer(String)
:这个Integer
构造函数接受一个String
作为参数,并用该字符串中给定的值创建一个Integer
对象。下面是如何使用该构造函数:
Function<String, Integer> anotherInteger = Integer::new;
System.out.println(anotherInteger.apply("100"));
// this code prints: 100
我们不能在这里使用Supplier
,因为Suppliers
不接受任何参数。Functions
接受参数,这里的返回类型是Integer
,因此我们可以使用Function<String, Integer>
。
函数式接口的原始版本
认证目标 |
---|
开发使用函数式接口原始版本的代码 |
内置接口Predicate
、Consumer
、Function
和Supplier
作用于引用类型对象。对于原始类型,这些功能接口的int
、long
和double
类型有可用的专门化。考虑对类型为T
的对象进行操作的Predicate
,即它是Predicate<T>
。int
、long
和double
对Predicate
的专门化分别是IntPredicate
、LongPredicate
和DoublePredicate
。
由于泛型的限制,您不能在函数式接口Predicate
、Consumer
、Function
和Supplier
中使用基元类型值。但是你可以使用包装器类型,比如Integer
和Double
来实现这些功能接口。当您试图在这些函数式接口中使用基本类型时,会导致隐式的自动装箱和取消装箱,例如,int
值被转换为Integer
对象,反之亦然。事实上,您甚至经常意识不到您正在使用带有这些功能接口的包装器类型。然而,当我们使用包装器类型时,性能会受到影响:想象一下在一个流中装箱和解箱几百万个整数。为了避免这个性能问题,您可以使用这些函数式接口的相关原语版本。
谓词接口的原始版本
考虑这个例子:
IntStream.range(1, 10).filter(i -> (i % 2) == 0).forEach(System.out::println);
这里的filter()
方法将一个IntPredicate
作为参数,因为底层流是一个IntStream
。下面是显式使用IntPredicate
的等价代码:
IntPredicate evenNums = i -> (i % 2) == 0;
IntStream.range(1, 10).filter(evenNums).forEach(System.out::println);
表 5-2 列出了java.util.function
包中提供的Predicate
接口的原始版本。
表 5-2。
Primitive Versions of Predicate Interface
功能接口 | 抽象方法 | 简要描述 |
---|---|---|
IntPredicate |
boolean test(int value) |
评估作为int 传递的条件,并返回一个boolean 值作为结果 |
LongPredicate |
boolean test(long value) |
评估作为long 传递的条件,并返回一个boolean 值作为结果 |
DoublePredicate |
boolean test(double value) |
评估作为double 传递的条件,并返回一个boolean 值作为结果 |
函数式接口的原始版本
下面是一个将Stream
用于原始类型 integers 的例子:
AtomicInteger ints = new AtomicInteger(0);
Stream.generate(ints::incrementAndGet).limit(10).forEach(System.out::println);
// prints integers from 1 to 10 on the console
这段代码调用在类java.util.concurrent.atomic.AtomicInteger
中定义的int incrementAndGet()
方法。注意,这个方法返回一个int
,而不是一个Integer
。尽管如此,我们还是可以将它与Stream
一起使用,因为它隐含了与int
的包装器类型Integer
之间的自动装箱和取消装箱。这种装箱和拆箱完全没有必要。相反,你可以使用IntStream
接口;它的generator()
方法以一个IntSupplier
作为参数。经过这一更改,下面是等效的代码:
AtomicInteger ints = new AtomicInteger(0);
IntStream.generate(ints::incrementAndGet).limit(10).forEach(System.out::println);
// prints integers from 1 to 10 on the console
因为他的代码使用了IntStream
,generate()
方法取了一个IntSupplier
,所以没有隐式的装箱和拆箱;因此这段代码执行得更快,因为它不会生成不必要的临时Integer
对象。
再举一个例子,下面是我们之前看到的使用Math.abs()
方法的一段代码:
Function<Integer, Integer> absInt = Math::abs;
你可以使用Function
的int
特殊化来替换它,称为IntFunction
:
IntFunction absInt = Math::abs;
根据参数和返回类型的不同,有许多版本的Function
接口原语类型(见表 5-3 )。
表 5-3。
Primitive Versions of Function Interface
功能接口 | 抽象方法 | 简要描述 |
---|---|---|
IntFunction<R> |
R apply(int value) |
对传递的int 参数进行操作,并返回泛型类型的值R |
LongFunction<R> |
R apply(long value) |
对传递的long 参数进行操作,并返回泛型类型的值R |
DoubleFunction<R> |
R apply(double value) |
对传递的double 参数进行操作,并返回泛型类型的值R |
ToIntFunction<T> |
int applyAsInt(T value) |
对传递的泛型类型参数T 进行操作,并返回一个int 值 |
ToLongFunction<T> |
long applyAsLong(T value) |
对传递的泛型类型参数T 进行操作,并返回一个long 值 |
ToDoubleFunction<T> |
double applyAsDouble(T value) |
对传递的泛型类型参数T 进行操作,并返回一个double 值 |
IntToLongFunction |
long applyAsLong(int value) |
对传递的int 类型参数进行操作,并返回一个long 值 |
IntToDoubleFunction |
double applyAsDouble(int value) |
对传递的int 类型参数进行操作,并返回一个double 值 |
LongToIntFunction |
int applyAsInt(long value) |
对传递的long 类型参数进行操作,并返回一个int 值 |
LongToDoubleFunction |
double applyAsLong(long value) |
对传递的long 类型参数进行操作,并返回一个double 值 |
DoubleToIntFunction |
int applyAsInt(double value) |
对传递的double 类型参数进行操作,并返回一个int 值 |
DoubleToLongFunction |
long applyAsLong(double value) |
对传递的double 类型参数进行操作,并返回一个long 值 |
消费者界面的原始版本
根据参数的种类,有许多版本的Consumer
接口原语类型可用(见表 5-4 )。
表 5-4。
Primitive Versions of Consumer Interface
功能接口 | 抽象方法 | 简要描述 |
---|---|---|
IntConsumer |
void accept(int value) |
对给定的int 参数进行操作,不返回任何内容 |
LongConsumer |
void accept(long value) |
对给定的long 参数进行操作,不返回任何内容 |
DoubleConsumer |
void accept(double value) |
对给定的double 参数进行操作,不返回任何内容 |
ObjIntConsumer<T> |
void accept(T t, int value) |
对给定的泛型类型参数T 和int 进行操作,不返回任何内容 |
ObjLongConsumer<T> |
void accept(T t, long value) |
对给定的泛型类型参数T 和long 进行操作,不返回任何内容 |
ObjDoubleConsumer<T> |
void accept(T t, double value) |
对给定的泛型类型参数T 和double 进行操作,不返回任何内容 |
供应商接口的原始版本
Supplier
的原始版本是分别返回boolean
、int
、long
、double
的BooleanSupplier
、IntSupplier
、LongSupplier
、DoubleSupplier
(见表 5-5 )。
表 5-5。
Primitive Versions of Supplier Interface
功能接口 | 抽象方法 | 简要描述 |
---|---|---|
BooleanSupplier |
boolean getAsBoolean() |
不接受任何参数并返回一个boolean 值 |
IntSupplier |
int getAsInt() |
不接受任何参数并返回一个int 值 |
LongSupplier |
long getAsLong() |
不接受任何参数并返回一个long 值 |
DoubleSupplier |
double getAsDouble() |
不接受任何参数并返回一个double 值 |
功能接口的原语版本仅适用于
int
、long,
和double
(除了Supplier
的这三种类型外,还有boolean
类型)。如果您需要一个接受或返回其他原语类型char
、byte
或short
的函数式接口,该怎么办?你必须使用隐式转换到相关的int
专门化。同样,当你使用float
时,你可以使用double
类型的专门化。
功能接口的二进制版本
认证目标 |
---|
开发使用函数式接口二进制版本的代码 |
函数式接口Predicate
、Consumer
和Function
具有带一个参数的抽象方法。例如,考虑一下Function
接口:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// other methods elided
}
抽象方法apply()
接受一个参数(泛型类型T
)。下面是二进制版本的Function
界面:
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
// other methods elided
}
A
BiFunction
类似于Function
,但不同之处在于它接受两个参数:接受泛型类型T
和U
的参数,并返回泛型类型R
的对象。你可以在一个BiFunction
对象上调用apply()
方法。
前缀“Bi”表示采用“两个”参数的版本。与Function
的BiFunction
相同,还有Predicate
的BiPredicate
和Consumer
的BiConsumer
需要两个参数(见表 5-6 )。Supplier
怎么样?因为Supplier
中的抽象方法不接受任何参数,所以没有等价的BiSupplier
可用。
表 5-6。
Binary Versions of Functional Interfaces
功能接口 | 抽象方法 | 简要描述 |
---|---|---|
BiPredicate<T, U> |
boolean test(T t, U u) |
检查参数是否与条件匹配,并返回一个boolean 值作为结果 |
BiConsumer<T, U> |
void accept(T t, U u) |
消耗两个参数但不返回任何内容的操作 |
BiFunction<T, U, R> |
R apply(T t, U u) |
接受两个参数并返回结果的函数 |
双功能接口
下面是一个使用BiFunction
接口的例子:
BiFunction<String, String, String> concatStr = (x, y) -> x + y;
System.out.println(concatStr.apply("hello ", "world"));
// prints: hello world
在此示例中,参数和返回类型是相同的类型,但它们可以不同,如:
BiFunction<Double, Double, Integer> compareDoubles = Double::compare;
System.out.println(compareDoubles.apply(10.0, 10.0));
// prints: 0
在这种情况下,参数类型是类型double
,返回类型是integer
。当传递的双精度值相等时,Double
类中的compare
方法返回 0,因此我们得到这个代码段的输出 0。
考虑到在java.util.function
包中有大量可用的函数式接口,为给定的上下文找到合适的函数式接口可能很棘手。例如,在我们之前的例子中,我们使用了BiFunction<Double, Double, Integer>
。相反,我们可以使用函数式接口ToIntBiFunction
,因为它返回一个int
。
双预测接口
考虑下面的代码段:
BiPredicate<List<Integer>, Integer> listContains = List::contains;
List aList = Arrays.asList(10, 20, 30);
System.out.println(listContains.test(aList, 20));
// prints: true
这段代码展示了如何使用BiPredicate
。List
中的contains()
方法将一个元素作为参数,并检查底层列表是否包含该元素。因为它接受一个参数并返回一个Integer
,所以我们可以使用一个BiPredicate
。为什么不用BiFunction<T, U, Boolean>
?是的,代码可以工作,但是更好的选择是等价的BiPredicate<T, U>
,因为BiPredicate
返回一个boolean
值。
双消费者界面
考虑这段代码:
BiConsumer<List<Integer>, Integer> listAddElement = List::add;
List aList = new ArrayList();
listAddElement.accept(aList, 10);
System.out.println(aList);
// prints: [10]
这段代码展示了如何使用BiConsumer
。类似于在前面的例子中为BiPredicate
使用List::contains
方法引用,这个例子展示了如何使用这个接口使用BiConsumer
来调用List
中的add()
方法。
一元运算符接口
认证目标 |
---|
开发使用一元运算符接口的代码 |
考虑下面的例子。
List<Integer> ell = Arrays.asList(-11, 22, 33, -44, 55);
System.out.println("Before: " + ell);
ell.replaceAll(Math::abs);
System.out.println("After: " + ell);
该代码打印:
Before: [-11, 22, 33, -44, 55]
After: [11, 22, 33, 44, 55]
这段代码使用 Java 8 中引入的replaceAll()
方法来替换给定List
中的元素。replaceAll()
方法将UnaryOperator
作为唯一的参数:
void replaceAll(UnaryOperator<T> operator)
将replaceAll()
方法与Math::abs
方法一起传递给它。
Math
对abs()
方法有四个重载方法:
abs(int)
abs(long)
abs(double)
abs(float)
因为类型是Integer
,所以通过类型推断选择重载的方法abs(int)
。
UnaryOperator
是一个函数式接口,它扩展了Function
接口,可以使用Function
接口中声明的apply()
方法;此外,它从Function
接口继承了默认功能compose()
和andThen()
。类似于UnaryOperator
扩展了Function
接口,还有一个BinaryOperator
扩展了BiFunction
接口。
接口IntUnaryOperator
、LongUnaryOperator
和DoubleUnaryOperator
的原始类型版本也作为java.util.function
包的一部分提供。
Java . util . function 包只包含函数式接口。这个包里只有四个核心接口:
Predicate
、Consumer
、Function
和Supplier
。剩下的接口都是原语版本,二进制版本,以及衍生接口比如UnaryOperator
接口。这些接口的主要区别在于它们声明的抽象方法的签名。您需要根据上下文和您的需求选择合适的功能接口。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
使用 java.util.function 包中包含的内置接口,如谓词、使用者、函数和供应商
- 内置函数式接口
Predicate
、Consumer
、Function
和Supplier
的区别主要在于它们声明的抽象方法的签名。 - A
Predicate
测试给定的条件并返回true
或false
;因此,它有一个名为“test
”的抽象方法,该方法接受泛型类型T
的参数并返回类型boolean
。 - A
Consumer
“消费”一个对象,不返回任何东西;因此,它有一个名为“accept
”的抽象方法,该方法接受泛型类型T
的参数,并具有返回类型void
。 - A
Function
“操作”参数并返回结果;因此,它有一个名为“apply
”的抽象方法,该方法接受泛型类型T
的参数,并具有泛型返回类型R
。 - 一个
Supplier
“物资”什么都不拿,却要回报一些东西;因此,它有一个名为“get”的抽象方法,该方法不带参数,返回一个泛型类型T
。 - 在
Iterable
(由集合类实现)方法中定义的forEach()
方法接受一个Consumer<T>
。
开发使用函数式接口原始版本的代码
- 内置接口
Predicate
、Consumer
、Function
和Supplier
作用于引用类型对象。对于基本类型,这些函数式接口的int
、long
和double
类型都有专门化。 - 当
Stream
接口用于基本类型时,会导致不必要的基本类型到它们的包装器类型的装箱和拆箱。这会导致代码变慢,并浪费内存,因为会创建不必要的包装对象。因此,只要有可能,最好使用功能接口Predicate
、Consumer
、Function
和Supplier
的原始类型专门化。 - 功能接口
Predicate
、Consumer
、Function
、Supplier
的原语版本仅适用于int
、long
和double
类型(除了Supplier
的这三种类型外,还有boolean
类型)。当你需要使用char
、byte
或short
类型时,你必须使用到相关int
版本的隐式转换;同样,当需要使用float
时,可以使用double
类型的版本。
开发使用函数式接口二进制版本的代码
- 功能接口
BiPredicate
、BiConsumer
、BiFunction
分别是Predicate
、Consumer
、Function
的二进制版本。对于Supplier
没有等价的二进制,因为它不接受任何参数。前缀“Bi”表示采用“两个”参数的版本。
开发使用一元运算符接口的代码
UnaryOperator
是一个功能接口,它扩展了Function
接口。UnaryOperator
的原始类型专门化为IntUnaryOperator
、LongUnaryOperator
和DoubleUnaryOperator
,分别代表int
、long
和double
类型。
Question TimeWhich of the following are functional interfaces? (Select ALL that apply) java.util.stream.Streamimport java.util.function.Predicate;
public class PredicateTest {
public static void main(String []args) {
Predicate<String> notNull =
((Predicate<String>)(arg -> arg == null)).negate(); // #1
System.out.println(notNull.test(null));
}
}
This program results in a compiler error in line marked with the comment #1 This program prints: true This program prints: false This program crashes by throwing NullPointerException
Choose the correct option based on this program: import java.util.function.Function;
public class AndThen {
public static void main(String []args) {
Function<Integer, Integer> negate = (i -> -i), square = (i -> i * i),
negateSquare = negate.compose(square);
System.
out
.println(negateSquare.apply(10));
}
}
This program results in a compiler error This program prints: -100 This program prints: 100 This program prints: -10 This program prints: 10 Which one of the following functional interfaces can you assign the method reference Integer::parseInt
? Note that the static method parseInt()
in Integer
class takes a String
and returns an int
, as in: int parseInt(String s)
BiPredicate<String, Integer> Function<Integer, String> Function<String, Integer> Predicateimport java.util.function.BiFunction;
public class StringCompare {
public static void main(String args[]){
BiFunction<String, String, Boolean> compareString = (x, y) -> x.equals(y);
System.out.println(compareString.apply("Java8","Java8")); // #1
}
}
This program results in a compiler error in line marked with #1 This program prints: true This program prints: false This program prints: (x, y) -> x.equals(y)
This program prints: ("Java8", "Java8") -> "Java8".equals("Java8")
Which one of the following abstract methods does not take any argument but returns a value? The accept()
method in java.util.function.Consumer<T>
interface The get()
method in java.util.function.Supplier<T>
interface The test()
method in java.util.function.Predicate<T>
interface The apply()
method in java.util.function.Function<T, R>
interface Choose the correct option based on this program: import java.util.function.Predicate;
public class PredUse {
public static void main(String args[]){
Predicate<String> predContains = "I am going to write OCP8 exam"::contains;
checkString(predContains, "OCPJP");
}
static void checkString(Predicate<String> predicate, String str) {
System.out.println(predicate.test(str) ? "contains" : "doesn't contain");
}
}
This program results in a compiler error for code within main()
method This program results in a compiler error for code within checkString()
method This program prints: contains This program prints: doesn’t contain Choose the correct option based on this program: import java.util.function.ObjIntConsumer;
class ConsumerUse {
public static void main(String []args) {
ObjIntConsumer<String> charAt = (str, i) -> str.charAt(i); // #1
System.
out
.println(charAt.accept("java", 2)); // #2
}
}
This program results in a compiler error for the line marked with comment #1 This program results in a compiler error for the line marked with comment #2 This program prints: a This program prints: v This program prints: 2
答案:
B, C, D, and E The interface java.util.stream.Stream<T>
is not a functional interface–it has numerous abstract methods. The other four options are functional interfaces. The functional interface java.util.function.Consumer<T>
has an abstract method with the signature void accept(T t);
The functional interface java.util.function.Supplier<T>
has an abstract method with the signature T get();
The functional interface java.util.function.Predicate<T>
has an abstract method with the signature boolean test(T t);
The functional interface java.util.function.Function<T, R>
has an abstract method with the signature R apply(T t);
C. This program prints: false The expression ((Predicate<String>)(arg -> arg == null))
is a valid cast to the type (Predicate<String>)
for the lambda expression (arg -> arg == null)
. Hence, it does not result in a compiler error. The negate
function in Predicate
interface turns true
to false
and false
to true
. Hence, given null
, the notNull.test(null)
results in returning the value false
. B. This program prints: -100 The negate.compose(square)
calls square before calling negate. Hence, for the given value 10, square results in 100, and when negated, it becomes -100. C. Function<String, Integer> The parseInt()
method takes a String
and returns a value, hence we need to use the Function
interface because it matches the signature of the abstract method R apply(T t)
. In Function<T, R>
, the first type argument is the argument type and the second one is the return type. Given that parseInt
takes a String
as the argument and returns an int
(that can be wrapped in an Integer
), we can assign it to Function<String, Integer>
. B. This program prints: true The BiFunction
interface takes two type arguments–they are of types String
in this program. The return type is Boolean. BiFunction
functional interface has abstract method named apply()
. Since the signature of String
’s equals()
method matches that of the signature of the abstract method apply()
, this program compiles fine. When executed, the strings “Java8” and “Java8” are equal; hence, the evaluation returns true that is printed on the console. B. The get()
method in java.util.function.Supplier<T>
interface The signature of get()
method in java.util.function.Supplier<T>
interface is: T get()
. D. This program prints: doesn’t contain You can create method references for object as well, so the code within main()
compiles without errors. The code within checkString()
method is also correct and hence it also compiles without errors. The string “OCPJP” is not present in the string “I am going to write OCP8 exam” and hence this program prints “doesn’t contain” on the console. D. This program results in a compiler error for the line marked with comment #2 ObjIntConsumer
operates on the given String
argument str
and int
argument i
and returns nothing. Though the charAt
method is declared to return the char
at given index i
, the accept
method in ObjIntConsumer
has return type void
. Since System.out.println
expects an argument to be passed, the call charAt.accept("java", 2)
results in a compiler error because accept()
method returns void.
六、Java 流 API
认证目标 |
---|
开发代码以使用 peek()和 map()方法从对象中提取数据,包括 map()方法的原始版本 |
使用流类的搜索方法搜索数据,包括 findFirst、findAny、anyMatch、allMatch、noneMatch |
开发使用可选类的代码 |
开发使用流数据方法和计算方法的代码 |
使用流 API 对集合进行排序 |
使用 collect 方法将结果保存到集合中,使用 Collectors 类将数据分组/分区 |
在流 API 中使用 flatMap()方法 |
在本章中,我们将讨论 Java 8 中对 Java 库最重要的补充:stream API。流 API 是java.util.stream
包的一部分。本章的重点是这个包中的关键接口:Stream<T>
接口(及其原始类型版本)。在这一章中,我们还将讨论诸如Optional
和Collectors
这样的职业。
我们已经在第四章(泛型和集合)中介绍了 stream API。stream API 广泛使用了内置的函数式接口,这些接口是我们在前一章讨论的java.util.function
包的一部分(第五章)。所以,我们假设你在看本章之前已经看过这两章了。
从流中提取数据
认证目标 |
---|
开发代码以使用 peek()和 map()方法从对象中提取数据,包括 map()方法的原始版本 |
让我们从一个简单的例子开始:
long count = Stream.of(1, 2, 3, 4, 5).map(i -> i * i).count();
System.out.printf("The stream has %d elements", count);
这段代码打印了:
The stream has 5 elements
该流中的map()
操作将作为参数传递的给定 lambda 函数应用于流的元素。在这种情况下,它对流中的元素求平方。count()
方法返回值 5——您在一个变量中捕获它并在控制台上打印出来。但是你如何检查在这段代码中应用中间操作map()
的结果呢?为此,您可以使用peek()
方法:
long count = Stream.of(1, 2, 3, 4, 5)
.map(i -> i * i)
.peek(i -> System.out.printf("%d ", i))
.count();
System.out.printf("%nThe stream has %d elements", count);
这段代码打印出来
1 4 9 16 25
The stream has 5 elements
这个例子还说明了如何将中间操作链接在一起。这是可能的,因为中间操作返回流。
现在,让我们在调用map()
方法之前添加另一个peek()
方法,以了解它是如何工作的:
Stream.of(1, 2, 3, 4, 5)
.peek(i -> System.out.printf("%d ", i))
.map(i -> i * i)
.peek(i -> System.out.printf("%d ", i))
.count();
这段代码打印出来
1 1 2 4 3 9 4 16 5 25
从这个输出可以看出,流管道正在逐个处理元素。每个元素都映射到它的正方形。peek()
方法帮助我们理解流中正在处理什么,而不分发它。
peek()
方法主要用于调试目的。它有助于我们理解元素在管道中是如何转换的。不要在产品代码中使用它。
你可以在Stream<T>
的原始版本中使用map()
和peek()
方法;然后下面的代码片段使用了一个DoubleStream
:
DoubleStream.``of
.map(Math::``sqrt
.peek(System.``out
.sum();
此代码在控制台上的不同行中打印出 1.0、2.0 和 3.0。图 6-1 直观地显示了该流管道中的源、中间操作和终端操作。
图 6-1。
A stream pipeline with source, intermediate operations and terminal operation
从流中搜索数据
认证目标 |
---|
使用流类的搜索方法搜索数据,包括 findFirst、findAny、anyMatch、allMatch、noneMatch |
在Stream
界面中,以单词Match
结尾的方法和以单词find
开头的方法对于从流中搜索数据很有用(表 6-1 )。如果您在流中寻找匹配给定条件的元素,您可以使用匹配操作,例如anyMatch()
、allMatch()
和noneMatch()
。这些方法返回一个布尔值。对于搜索操作findFirst()
和findAny()
,匹配元素可能不在Stream
中,所以它们返回Optional<T>
(我们将在下一节讨论Optional<T>
)。
表 6-1。
Important Match and Find Methods in the Stream
Interface
方法名称 | 简短描述 |
---|---|
boolean anyMatch(Predicate<? super T> check) |
如果流中有任何元素匹配给定的谓词,则返回 true。如果流为空或者没有匹配的元素,则返回 false。 |
boolean allMatch(Predicate<? super T> check) |
仅当流中的所有元素都匹配给定的谓词时,才返回 true。如果流是空的,不计算谓词,则返回 true! |
boolean noneMatch(Predicate<? super T> check) |
仅当流中没有任何元素与给定谓词匹配时,才返回 true。如果流是空的,不计算谓词,则返回 true! |
Optional<T> findFirst() |
返回流中的第一个元素;如果流中没有元素,它返回一个空的Optional<T> 对象。 |
Optional<T> findAny() |
从流中返回一个元素;如果流中没有元素,它返回一个空的Optional<T> 对象。 |
与流为空时返回 false 的 anyMatch()方法不同,allMatch()和 noneMatch()方法在流为空时返回 true!
下面是一个简单的程序,演示了如何使用anyMatch()
、allMatch()
和noneMatch()
方法(清单 6-1 )。
Listing 6-1. MatchUse.java
import java.util.stream.IntStream;
public class MatchUse {
public static void main(String []args) {
// Average temperatures in Concordia, Antarctica in a week in October 2015
boolean anyMatch
= IntStream.of(-56, -57, -55, -52, -48, -51, -49).anyMatch(temp -> temp > 0);
System.out.println("anyMatch(temp -> temp > 0): " + anyMatch);
boolean allMatch
= IntStream.of(-56, -57, -55, -52, -48, -51, -49).allMatch(temp -> temp > 0);
System.out.println("allMatch(temp -> temp > 0): " + allMatch);
boolean noneMatch
= IntStream.of(-56, -57, -55, -52, -48, -51, -49).noneMatch(temp -> temp > 0);
System.out.println("noneMatch(temp -> temp > 0): " + noneMatch);
}
}
该程序打印:
anyMatch(temp -> temp > 0): false
allMatch(temp -> temp > 0): false
noneMatch(temp -> temp > 0): true
因为所有给定的温度都是负值,anyMatch()
和allMatch()
方法返回 false,而noneMatch()
返回 true。
findFirst()
和findAny()
方法对于在流中搜索元素很有用。这里有一个使用findFirst()
方法的程序(列表 6-2 )。
Listing 6-2. FindFirstUse1.java
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;
public class FindFirstUse1 {
public static void main(String []args) {
Method[] methods = Stream.class.getMethods();
Optional<String> methodName = Arrays.stream(methods)
.map(method -> method.getName())
.filter(name -> name.endsWith("Match"))
.sorted()
.findFirst();
System.out.println("Result: " + methodName.orElse("No suitable method found"));
}
}
该程序打印:
Result: allMatch
在这个程序中,我们使用反射获得了Stream
本身的方法列表。然后,使用map()
方法,我们获得方法名的列表,检查名称是否以字符串“Match”结尾,对这些方法进行排序,并返回第一个找到的方法。如果我们正在寻找任何以“匹配”结尾的方法名,那么我们可以使用findAny()
方法来代替。
为什么
java.util.function
包同时有findFirst()
和findAny()
方法?在并行流中,findAny()
比findFirst()
用起来更快(我们在第十一章中讨论并行流)。
清单 6-3 有一个流,它有许多温度值,都是双精度值。使用findFirst()
,我们寻找任何大于 0 的温度。程序将打印什么?
Listing 6-3. FindFirstUse2.java
import java.util.OptionalDouble;
import java.util.stream.DoubleStream;
public class FindFirstUse2 {
public static void main(String []args) {
OptionalDouble temperature = DoubleStream.of(-10.1, -5.4, 6.0, -3.4, 8.9, 2.2)
.filter(temp -> temp > 0)
.findFirst();
System.out.println("First matching temperature > 0 is " + temperature.getAsDouble());
}
}
该程序打印:
First matching temperature > 0 is 6.0
在这个双精度值流中,filter()
方法过滤元素10.1
和-5.4
,因为条件temp > 0
为假。对于元素 6.0,filter()
方法评估条件为真,并且findFirst()
返回该元素。注意,这个流管道中的其余元素被忽略了:元素 8.9 和 2.2 也满足条件temp > 0
,但是流管道被关闭,因为findFirst()
方法已经返回值 6.0。换句话说,像findFirst()
这样的搜索方法就是短路。一旦确定了结果,就不会处理流中的其余元素。
用于搜索元素的“匹配”和“查找”方法本质上是“短路”。什么是短路?一旦找到结果,评估就停止(并且不评估其余部分)。您已经熟悉了运算符& &和||“短路”的名称。例如,在表达式((s!= null) & & (s.length() > 0)),如果字符串 s 为 null,则条件(s!= null)计算结果为 false 因此,表达式的结果为 false。在这种情况下,不计算剩余的表达式(s.length() > 0)。
在清单 6-2 和 6-3 中,我们使用了Optional
和OptionalDouble
类;现在让我们来讨论这两个类。
选修课
认证目标 |
---|
开发使用可选类的代码 |
类java.util.Optional
是值的持有者,该值可以是null
。在java.util.stream
包的类中有许多方法返回Optional
值。现在让我们看一个例子。
考虑这种方法:
public static void selectHighestTemperature(Stream<Double> temperatures) {
System.out.println(temperatures.max(Double::compareTo));
}
下面是对此方法的调用:
selectHighestTemperature(Stream.of(24.5, 23.6, 27.9, 21.1, 23.5, 25.5, 28.3));
该代码打印:
Optional[28.3]
Stream
中的max()
方法将一个Comparator
作为参数,并返回一个Optional<T>:
Optional<T> max(Comparator<? super T> comparator);
为什么用Optional<T>
而不是返回类型T
?这是因为max()
方法可能无法找到最大值——例如,考虑一个空流:
selectHighestTemperature(Stream.of());
现在,这段代码打印出来:
Optional.empty
要从Optional
获取值,可以使用isPresent()
和get()
方法,如下所示:
public static void selectHighestTemperature(Stream<Double> temperatures) {
Optional<Double> max = temperatures.max(Double::compareTo);
if(max.isPresent()) {
System.out.println(max.get());
}
}
编写一个if
条件是繁琐的(并且不是函数式的),所以可以使用ifPresent
方法来编写简化的代码:
max.ifPresent(System.out::println);
Optional
中的这个ifPresent()
方法以一个Consumer<T>
作为参数。你也可以使用像orElse()
和orElseThrow()
这样的方法,我们将在讨论如何创建Optional
对象之后讨论这些方法。
创建可选对象
有许多方法可以创建Optional
对象。创建Optional
对象的一种方法是在Optional
类中使用工厂方法,如下所示:
Optional<String> empty = Optional.empty();
也可以在Optional
类中使用of()
:
Optional<String> nonEmptyOptional = Optional.of("abracadabra");
但是,您不能将null
传递给Optional.of()
方法,如:
Optional<String> nullStr = Optional.of(null);
System.out.println(nullStr);
// crashes with a NullPointerException
这将导致抛出一个NullPointerException
。如果你想创建一个有null
值的Optional
对象,那么你可以使用ofNullable()
方法:
Optional<String> nullableStr = Optional.ofNullable(null);
System.out.println(nullableStr);
// prints: Optional.empty
图 6-2 将nonEmptyOptional
、nullStr
和nullableStr
所指向的Optional<String>
对象形象化表示。
图 6-2。
Representation of three Optional
可选流
你也可以把Optional
看作一个可以有零个元素或者一个元素的流。所以你可以在这个流上应用诸如map()
、filter()
和flatMap()
操作的方法!怎么有用?下面是一个例子(列表 6-4 ):
Listing 6-4. OptionalStream.java
import java.util.Optional;
public class OptionalStream {
public static void main(String []args) {
Optional<String> string = Optional.of(" abracadabra ");
string.map(String::trim).ifPresent(System.out::println);
}
}
该程序打印:
abracadabra
当这些操作失败时,您可以使用orElse()
或orElseThrow()
方法(例如,底层的Optional
具有空值),如下所示:
Optional<String> string = Optional.ofNullable(null);
System.out.println(string.map(String::length).orElse(-1));
这段代码输出-1,因为变量string
是保存null
的Optional
变量,因此orElse()
方法执行并返回-1。或者,您可以使用orElseThrow()
方法抛出一个异常:
Optional<String> string = Optional.ofNullable(null);
System.out.println(string.map(String::length).orElseThrow(IllegalArgumentException::new));
这段代码抛出了一个IllegalArgumentException
。当你处理从一个函数返回的Optional
对象,而你不知道Optional
对象包含什么的时候,在一个Optional
对象上调用map()
、flatMap()
或filter()
这样的方法是很有用的。
可选的原始版本
在我们之前讨论的代码中,我们同时使用了Stream<Double>
和Optional<Double>
类型:
public static void selectHighestTemperature(Stream<Double> temperatures) {
Optional<Double> max = temperatures.max(Double::compareTo);
if(max.isPresent()) {
System.out.println(max.get());
}
}
最好用DoubleStream
和OptionalDouble
,分别是Stream<T>
和Optional<T>
的double
的原语类型版本。(另外两个可用的原语类型版本用于int
和long
,分别命名为OptionalInt
和OptionalLong
。)所以,这段代码可以重写为:
public static void selectHighestTemperature(DoubleStream temperatures) {
OptionalDouble max = temperatures.max();
max.ifPresent(System.out::println);
}
当用下面的调用调用时,
selectHighestTemperature(DoubleStream.of(24.5, 23.6, 27.9, 21.1, 23.5, 25.5, 28.3));
我们得到控制台上正确显示的最大值:
28.3
类似于返回Optional<T>
的max()
方法Stream<T>
,DoubleStream
中的max()
方法返回一个OptionalDouble
。
流数据方法和计算方法
认证目标 |
---|
开发使用流数据方法和计算方法的代码 |
Stream<T>
接口有数据和计算方法count()
、min()
和max()
。min()
和max()
方法将一个Comparator
对象作为参数并返回一个Optional<T>
。这里有一个使用这些方法的例子(清单 6-5 )。
Listing 6-5. WordsCalculation.java
import java.util.Arrays;
public class WordsCalculation {
public static void main(String []args) {
String[] string = "you never know what you have until you clean your room".split(" ");
System.out.println(Arrays.stream(string).min(String::compareTo).get());
}
}
该程序打印:
clean
因为min()
方法需要一种方法来比较流中的元素,所以我们在这个程序中传递了String::compareTo
方法引用。由于min()
返回一个Optional<T>
,我们使用了get()
方法来获得结果字符串。由于String::compareTo
按字典顺序比较两个字符串,我们得到单词“clean”作为结果。
下面是修改后的代码片段,它不是按字典顺序而是根据字符串的长度来比较字符串:
Comparator<String> lengthCompare = (str1, str2) -> str1.length() - str2.length();
System.out.println(Arrays.stream(string).min(lengthCompare).get());
有了这个改变,程序打印出“you ”,因为它是给定的string
中长度最小的单词。
在Stream<T>
接口的原始版本中提供了额外的数据和计算方法,如sum()
和average()
。表 6-2 列出了我们在本节讨论的IntStream
接口中的重要方法。
表 6-2。
Important Data and Calculation Methods in IntStream
Interface
方法 | 简短描述 |
---|---|
int sum() |
返回流中元素的总和;如果流为空,则为 0。 |
long count() |
返回流中元素的数量;如果流为空,则为 0。 |
OptionalDouble average() |
返回流中元素的平均值;如果流为空,则为空的OptionalDouble 值。 |
OptionalInt min() |
返回流中的最小整数值;如果流为空,则为空的OptionalInt 值。 |
OptionalInt max() |
返回流中的最大整数值;如果流为空,则为空的OptionalInt 值。 |
IntSummaryStatistics summaryStatistics() |
返回一个具有sum 、count 、average 、min 和max 值的IntSummaryStatistics 对象。 |
LongStream
和DoubleStream
接口的方法与本表中为IntStream
列出的方法相似(表 6-2 )。下面是一个使用它们的简单程序(清单 6-6 )。
Listing 6-6. WordStatistics.java
import java.util.IntSummaryStatistics;
import java.util.regex.Pattern;
public class WordStatistics {
public static void main(String []args) {
String limerick = "There was a young lady named Bright " +
"who traveled much faster than light " +
"She set out one day " +
"in a relative way " +
"and came back the previous night ";
IntSummaryStatistics wordStatistics =
Pattern.compile(" ")
.splitAsStream(limerick)
.mapToInt(word -> word.length())
.summaryStatistics();
System.out.printf(" Number of words = %d \n Sum of the length of the words = %d \n" +
" Minimum word size = %d \n Maximum word size %d \n " +
" Average word size = %f \n", wordStatistics.getCount(),
wordStatistics.getSum(), wordStatistics.getMin(),
wordStatistics.getMax(), wordStatistics.getAverage());
}
}
该程序打印:
Number of words = 28
Sum of the length of the words = 115
Minimum word size = 1
Maximum word size 8
Average word size = 4.107143
在使用Pattern
类中的splitAsStream()
方法将单词分割成一个流之后,这个程序调用mapToInt()
方法将单词转换成它们的长度。为什么用mapToInt()
而不是map()
的方法?map()
方法返回一个Stream
,但是我们想要在流中的底层元素上执行计算。接口没有执行计算的方法,但是它的原始类型版本有数据和计算方法。因此,我们称返回IntStream
的mapToInt()
方法:IntStream
有许多有用的数据和计算方法(列于表 6-2 )。我们已经在IntStream
上调用了summaryStatistics()
方法。最后,我们在返回的IntSummaryStatistics
对象上调用了各种方法,如sum()
和average()
来总结对给定打油诗中使用的单词的计算。
也可以直接调用IntStream
中提供的sum()
和average()
等方法,如:
IntStream.of(10, 20, 30, 40).sum();
这些方法比使用reduce()
方法的等效方法更简洁:
IntStream.of(10, 20, 30, 40).reduce(0, ((sum, val) -> sum + val));
为什么 stream API 提供了reduce()
方法,而我们可以使用像sum()
这样更简洁,更方便使用,也更易读的方法?
答案是reduce()
是一个一般化的方法:当你想对 stream 元素执行重复操作来计算一个结果时,可以使用它。考虑 10 的阶乘。我们没有像IntStream
中的sum()
那样的方法可以帮助我们将所有的值相乘。因此,我们可以在这种情况下使用reduce()
方法:
// factorial of 5
System.out.println(IntStream.rangeClosed(1, 5).reduce((x, y) -> (x * y)).getAsInt());
// prints: 120
实际上,IntStream
的sum()
方法是通过调用reduce()
方法(在IntPipeline
类中)在内部实现的:
@Override
public final int sum() {
return reduce(0, Integer::sum);
}
在这种情况下,sum()
方法通过将方法引用Integer::sum
作为第二个参数传递给reduce()
方法来实现。
归约操作(又名“归约器”)可以是隐式的,也可以是显式的。
IntStream
中的sum()
、min()
和max()
等方法就是隐式归约器的例子。当我们在代码中直接使用reduce()
方法时,我们使用的是显式归约器。我们可以将隐式归约器转换为等价的显式归约器。
使用流 API 对集合进行排序
认证目标 |
---|
使用流 API 对集合进行排序 |
在第四章(关于泛型和集合)中,我们讨论了如何使用Comparator
和Comparable
接口对集合进行排序。流简化了对集合进行排序的任务。下面是一个用字典式比较对字符串进行排序的程序(清单 6-7 )。
Listing 6-7. SortingCollection.java
import java.util.Arrays;
import java.util.List;
public class SortingCollection {
public static void main(String []args) {
List words =
Arrays.``asList
words.stream().distinct().sorted().forEach(System.out::println);
}
}
该程序打印:
brain
but
follow
heart
take
with
you
your
在这段代码中,words
是一个类型为List
的集合。我们首先使用stream()
方法从列表中获取一个流,然后调用distinct()
方法删除重复项(单词“your”在集合中重复出现)。之后,我们称之为sorted()
法。
sorted()
方法按照元素的“自然顺序”对它们进行排序;sorted()
方法要求流中的元素实现Comparable
接口。如何以其他顺序对元素进行排序?为此,您可以调用以Comparator
作为参数的重载排序方法:
Stream<T> sorted(Comparator<? super T> comparator)
这里(清单 6-8 )是早期程序(在清单 6-7 中)的修改版本,它根据字符串的长度对元素进行排序。
Listing 6-8. SortByLength.java
import java.util.Arrays;
import java.util.List;
import java.util.Comparator;
public class SortByLength {
public static void main(String []args) {
List words =
Arrays.asList("follow your heart but take your brain with you".split(" "));
Comparator<String> lengthCompare = (str1, str2) -> str1.length() - str2.length();
words.stream().distinct().sorted(lengthCompare).forEach(System.out::println);
}
}
该程序打印:
but
you
your
take
with
heart
brain
follow
在这个输出中,单词根据单词的长度进行排序。“心”这个词出现在“脑”之前,尽管它们的长度相同。那么,如果我们想先把单词按长度排序,然后再把同样长度的单词按自然顺序排序呢?为此,您可以使用Comparator
接口中提供的thenComparing()
默认方法(列表 6-9 )。
Listing 6-9. SortByLengthThenNatural.java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class SortByLengthThenNatural {
public static void main(String []args) {
List words =
Arrays.``asList
Comparator<String> lengthCompare = (str1, str2) -> str1.length() - str2.length();
words.stream()
.distinct()
.sorted(lengthCompare.thenComparing(String::compareTo))
.forEach(System.``out
}
}
该程序打印:
but
you
take
with
your
brain
heart
follow
如果我们想颠倒这个顺序呢?幸运的是,在 Java 8 中,Comparator
接口已经通过许多有用的默认和静态方法得到了增强。添加的一个这样的方法是reversed()
,您可以利用它(清单 6-10 )。
Listing 6-10. SortByLengthThenNaturalReversed.java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class SortByLengthThenNaturalReversed {
public static void main(String []args) {
List words =
Arrays.asList("follow your heart but take your brain with you".split(" "));
Comparator<String> lengthCompare = (str1, str2) -> str1.length() - str2.length();
words.stream()
.distinct()
.sorted(lengthCompare.thenComparing(String::compareTo).reversed())
.forEach(System.out::println);
}
}
该程序打印:
follow
heart
brain
your
with
take
you
but
将结果保存到集合
认证目标 |
---|
使用 collect 方法将结果保存到集合中,使用 Collectors 类将数据分组/分区 |
Collectors
类具有支持将元素收集到集合中的任务的方法。您可以使用toList()
、toSet()
、toMap()
和toCollection()
等方法从流中创建一个集合。下面是一个简单的例子,它从一个流中创建一个List
并返回它(清单 6-11 )。这段代码使用了Stream
的collect()
方法和Collectors
类的toList()
方法。
Listing 6-11. CollectorsToList.java
import java.util.stream.Collectors;
import java.util.regex.Pattern;
import java.util.List;
public class CollectorsToList {
public static void main(String []args) {
String frenchCounting = "un:deux:trois:quatre";
List gmailList = Pattern.compile(":")
.splitAsStream(frenchCounting)
.collect(Collectors.toList());
gmailList.forEach(System.out::println);
}
}
The collect() method in Stream takes a Collector as an argument:
<R, A> R collect(Collector<? super T, A, R> collector);
在这段代码中,我们使用了 Collectors 类中的toList()
方法将流中的元素收集到一个列表中。
这里有一个使用Collectors.toSet()
方法的例子(列表 6-12 ):
Listing 6-12. CollectorsToSet.java
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class CollectorsToSet {
public static void main(String []args) {
String []roseQuote = "a rose is a rose is a rose".split(" ");
Set words = Arrays.stream(roseQuote).collect(Collectors.toSet());
words.forEach(System.out::println);
}
}
该程序打印:
a
rose
is
这段代码将字符串中的给定句子转换成单词流。在collect()
方法中调用的Collectors.toSet()
方法将单词收集到一个Set
中。由于 a Set
删除了重复项,这个程序只将单词“a”、“rose”和“is”打印到控制台。
就像Lists
和Sets
一样,你也可以从流中创建Maps
。下面是一个从字符串流中创建一个Map
的程序(列表 6-13 )。
Listing 6-13. CollectorsToMap.java
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CollectorsToMap {
public static void main(String []args) {
Map<String, Integer> nameLength = Stream.of("Arnold", "Alois", "Schwarzenegger")
.collect(Collectors.toMap(name -> name, name -> name.length()));
nameLength.forEach((name, len) -> System.out.printf("%s - %d \n", name, len));
}
}
该程序打印:
Alois - 5
Schwarzenegger - 14
Arnold - 6
Collectors.toMap()
方法有两个参数——第一个是键,第二个是值。这里,我们使用流本身中的元素作为键,字符串的长度作为值。你有没有注意到字符串“阿诺德”、“阿洛伊斯”和“施瓦辛格”在流中的顺序没有保留?这是因为Map
没有保持元素的插入顺序。
在这段代码中,注意我们使用了name -> name
:
Collectors.toMap(name -> name, name -> name.length())
我们可以通过传递Function.identity()
来简化它,比如:
Collectors.toMap(Function.identity(), name -> name.length())
回想一下Function
接口中的identity()
方法返回它接收到的参数(在第五章中讨论)。
如果您想使用一个特定的集合——比如说TreeSet
——来聚合来自collect()
方法的元素,该怎么办呢?为此,您可以使用Collections.toCollection()
方法,并将TreeSet
的构造函数引用作为参数传递(清单 6-14 )。
Listing 6-14. CollectorsToTreeSet.java
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
public class CollectorsToTreeSet {
public static void main(String []args) {
String []roseQuote = "a rose is a rose is a rose".split(" ");
Set words = Arrays.stream(roseQuote).collect(Collectors.toCollection(TreeSet::new));
words.forEach(System.out::println);
}
}
该程序打印:
a
is
rose
记住,TreeSet
对元素排序,因此输出是有序的。
您还可以根据某些标准对流中的元素进行分组(清单 6-15 )。
Listing 6-15. GroupStringsByLength.java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class GroupStringsByLength {
public static void main(String []args) {
String []string= "you never know what you have until you clean your room".split(" ");
Stream<String> distinctWords = Arrays.stream(string).distinct();
Map<Integer, List<String>> wordGroups =
distinctWords.collect(Collectors.groupingBy(String::length));
wordGroups.forEach(
(count, words) -> {
System.out.printf("word(s) of length %d %n", count);
words.forEach(System.out::println);
});
}
}
该程序打印:
word(s) of length 3
you
word(s) of length 4
know
what
have
your
room
word(s) of length 5
never
until
clean
Collectors
类中的groupingBy()
方法将一个Function
作为参数。它使用函数的结果返回一个Map
。Map
对象由匹配元素的Function
和List
返回的值组成。
如果你想把较长的单词和较小的单词分开呢?为此,您可以使用Collectors
类中的partitioningBy()
方法(清单 6-16 )。分区方法以一个Predicate
作为参数。
Listing 6-16. PartitionStrings.java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class PartitionStrings {
public static void main(String []args) {
String []string= "you never know what you have until you clean your room".split(" ");
Stream<String> distinctWords = Arrays.``stream
Map<Boolean, List<String>> wordBlocks =
distinctWords.collect(Collectors.``partitioningBy
System.``out
System.``out
}
}
该程序打印:
Short words (len <= 4): [you, know, what, have, your, room]
Long words (len > 4): [never, until, clean]
在partitioningBy()
方法中,我们已经给出了条件str -> str.length() > 4
。现在,结果将被分成两部分:一部分包含针对此条件评估为 true 的元素,另一部分评估为 false。在这种情况下,我们使用了partitioningBy()
方法将单词分为小单词(带有length <= 4
的单词)和长单词(带有length > 4
的单词)。
方法
groupingBy()
和partitioningBy()
有什么不同?groupingBy()
方法采用一个分类函数(类型为Function
),并基于分类函数返回输入元素及其匹配条目(并将结果组织在一个Map<K, List<T>>
)。partitioningBy()
方法将一个Predicate
作为参数,并根据给定的Predicate
将条目分类为真和假(并将结果组织在一个Map<Boolean, List<T>>
中)。
在流中使用平面图方法
认证目标 |
---|
在流 API 中使用 flatMap()方法 |
在前面的程序中,我们在调用split()
方法后发现字符串中有不同的单词:
String []string= "you never know what you have until you clean your room".split(" ");
Stream<String> distinctWords = Arrays.``stream
如果我们想在句子中找到不同的(独特的)字符呢?这个代码怎么样,有用吗?
String []string= "you never know what you have until you clean your room".split(" ");
Arrays.stream(string)
.map(word -> word.split(""))
.distinct()
.forEach(System.out::print);
这段代码打印出类似这样的乱码:
Ljava.lang.String;@5f184fc6[Ljava.lang.String;@3feba861[Ljava.lang.String;@5b480cf9[
为什么呢?因为word.split()
返回一个String[]
并且distinct()
删除重复的引用。因为流中的元素是类型String[]
,所以forEach()
prints 调用默认的toString()
实现来打印一些人类不可读的东西。
解决这个问题的一个方法是在word.split("")
上再次使用Arrays.stream()
,并将结果流转换成单独的条目(即“展平”流),如:flatMap(word -> Arrays.stream(word.split("")))
。经过这一修改,下面是打印句子中唯一字符的程序(清单 6-17 )。
Listing 6-17. UniqueCharacters.java
import java.util.Arrays;
public class UniqueCharacters {
public static void main(String []args) {
String []string= "you never know what you have until you clean your room".split(" ");
Arrays.stream(string)
.flatMap(word -> Arrays.stream(word.split("")))
.distinct()
.forEach(System.out::print);
}
}
该程序可以正确打印:
younevrkwhatilcm
让我们讨论一个例子,它清楚地说明了map()
和flatMap()
方法之间的区别(清单 6-18 和 6-19 )。
Listing 6-18. UsingMap.java
import java.util.Arrays;
import java.util.List;
public class UsingMap {
public static void main(String []args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.stream()
.map(i -> i * i)
.forEach(System.out::println);
}
}
该程序打印:
1
4
9
16
25
在这个程序中,我们有一个值为 1 到 5 的List<Integer>
。既然有了Integer
个元素,就可以直接调用map()
方法,将元素转换成它们的平方值(见图 6-3 )。
图 6-3。
The map() method transforms elements in a stream
现在,如果我们有一个List<Integer>
的List
,事情就变得困难了,如清单 6-19 所示。
Listing 6-19. UsingFlatMap.java
import java.util.Arrays;
import java.util.List;
public class UsingFlatMap {
public static void main(String []args) {
List<List<Integer>> intsOfInts = Arrays.asList(
Arrays.asList(1, 3, 5),
Arrays.asList(2, 4));
intsOfInts.stream()
.flatMap(ints -> ints.stream())
.sorted()
.map(i -> i * i)
.forEach(System.out::println);
}
}
该程序的输出与之前的程序相同(列表 6-18 )。它还打印值 1 到 5 的平方。
在这个程序中,我们有一个变量intsOfInts
,它是List<Integer>
的一个List
。当你在intsOfInts
上调用stream()
方法时,元素的类型会是什么?会是List<Integer>
。我们如何处理List<Integer>
中的元素?为此,一种方法是对它的每个元素调用stream()
方法。为了将这些流转换成Integer
元素,我们调用flatMap()
方法。在调用了flatMap()
之后,我们有了一个Integer
的流。我们现在可以执行像sorted()
和map()
这样的操作来处理或转换这些元素。图 6-4 直观显示了Stream
中map()
与flatMap()
方法的区别。
图 6-4。
The flatMap() method flattens the streams
flatMap()
方法像map()
方法一样对元素进行操作。然而,flatMap()
将每个元素映射到一个平面流中得到的流变平。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
开发代码以使用 peek()和 map()方法从对象中提取数据,包括 map()方法的原始版本
peek()
方法对于调试很有用:它帮助我们理解元素在管道中是如何转换的。- 您可以使用
map()
方法转换(或者只是提取)流中的元素
使用流类的搜索方法搜索数据,包括 findFirst、findAny、anyMatch、allMatch、noneMatch
- 您可以使用
allMatch()
、noneMatch()
和anyMatch()
方法匹配流中的给定谓词。与流为空时返回 false 的anyMatch()
方法不同,allMatch()
和noneMatch()
方法在流为空时返回 true。 - 您可以使用
findFirst()
和findAny()
方法在流中寻找元素。在并行流的情况下,findAny()
方法比findFirst()
方法更快。 - “
match
”和“find
”方法“短路”:一旦找到结果,求值就停止,流的其余部分不求值。
开发使用可选类的代码
- 当流中没有条目并且调用了 max 等操作时,Java 8 中采用的(更好的)方法是返回
Optional
值,而不是返回null
或抛出异常。 int
、long
和double
的Optional<T>
的原始类型版本分别为OptionalInteger
、OptionalLong
和OptionalDouble
。
开发使用流数据方法和计算方法的代码
Stream<T>
接口有数据和计算方法count()
、min()
和max()
;当调用这些min()
和max()
方法时,需要传递一个Comparator
对象作为参数。- 流接口的原始类型版本有以下数据和计算方法:
count()
、sum()
、average()
、min()
、max()
。 IntStream
、LongStream
和DoubleStream
中的summaryStatistics()
方法具有计算流中元素的计数、总和、平均值、最小值和最大值的方法。
使用流 API 对集合进行排序
- 对集合进行排序的一种方式是从集合中获取一个流,并对该流调用
sorted()
方法。sorted()
方法按照自然顺序对流中的元素进行排序(它要求流元素实现Comparable
接口)。 - 当您想要对流中的元素进行排序而不是自然顺序时,您可以将一个
Comparator
对象传递给sorted()
方法。 - 在 Java 8 中,
Comparator
接口已经通过许多有用的静态或默认方法得到了增强,比如thenComparing()
和reversed()
方法。
使用 collect 方法将结果保存到集合中,使用 Collectors 类将数据分组/分区
Collectors
类的collect()
方法具有支持将元素收集到集合中的任务的方法。Collectors
类提供了诸如toList()
、toSet()
、toMap()
和toCollection()
之类的方法来从流中创建集合。- 您可以使用
Collectors.groupingBy()
方法将流中的元素分组,并将分组标准(以Function
的形式给出)作为参数传递。 - 您可以使用
Collectors
类中的partition()
方法,根据条件(作为Predicate
给出)来分离流中的元素。。
使用流 API 的 flatMap()方法
Stream
中的flatMap()
方法将每个元素映射到一个平面流中所产生的流变平。
Question TimeChoose the best option based on this code segment: "abracadabra".chars().distinct().peek(ch -> System.
out
.printf("%c ", ch)).sorted();
It prints: “a b c d r” It prints: “a b r c d” It crashes by throwing a java.util.IllegalFormatConversionException
This code segment terminates normally without printing any output in the console Choose the best option based on this program: import java.util.function.IntPredicate;
import java.util.stream.IntStream;
public class MatchUse {
public static void main(String []args) {
IntStream temperatures = IntStream.of(-5, -6, -7, -5, 2, -8, -9);
IntPredicate positiveTemperature = temp -> temp > 0; // #1
if(temperatures.anyMatch(positiveTemperature)) { // #2
int temp = temperatures
.filter(positiveTemperature)
.findAny()
.getAsInt(); // #3
System.out.println(temp);
}
}
}
This program results in a compiler error in line marked with comment #1 This program results in a compiler error in line marked with comment #2 This program results in a compiler error in line marked with comment #3 This program prints: 2 This program crashes by throwing java.lang.IllegalStateException
Choose the best option based on this program: import java.util.stream.Stream;
public class AllMatch {
public static void main(String []args) {
boolean result = Stream.of("do", "re", "mi", "fa", "so", "la", "ti")
.filter(str -> str.length() > 5) // #1
.peek(System.out::println) // #2
.allMatch(str -> str.length() > 5); // #3
System.out.println(result);
}
}
This program results in a compiler error in line marked with comment #1 This program results in a compiler error in line marked with comment #2 This program results in a compiler error in line marked with comment #3 This program prints: false This program prints the strings “do”, “re”, “mi”, “fa”, “so”, “la”, “ti”, and “false” in separate lines This program prints: true Choose the best option based on this program: import java.util.*;
class Sort {
public static void main(String []args) {
List<String> strings = Arrays.asList("eeny ", "meeny ", "miny ", "mo ");
Collections.sort(strings, (str1, str2) -> str2.compareTo(str1));
strings.forEach(string -> System.out.print(string));
}
}
Compiler error: improper lambda function definition This program prints: eeny meeny miny mo This program prints: mo miny meeny eeny This program will compile fine, and when run, will crash by throwing a runtime exception. Choose the best option based on this program: import java.util.regex.Pattern;
import java.util.stream.Stream;
public class SumUse {
public static void main(String []args) {
Stream<String> words = Pattern.compile(" ").splitAsStream("a bb ccc");
System.out.println(words.map(word -> word.length()).sum());
}
}
Compiler error: Cannot find symbol “sum” in interface Streamjava.lang.IllegalStateException
Choose the best option based on this program: import java.util.OptionalInt;
import java.util.stream.IntStream;
public class FindMax {
public static void main(String args[]) {
maxMarks(IntStream.of(52,60,99,80,76)); // #1
}
public static void maxMarks(IntStream marks) {
OptionalInt max = marks.max(); // #2
if(max.ifPresent()) { // #3
System.out.print(max.getAsInt());
}
}
}
This program results in a compiler error in line marked with comment #1 This program results in a compiler error in line marked with comment #2 This program results in a compiler error in line marked with comment #3 This program prints: 99 Choose the best option based on this program: import java.util.Optional;
import java.util.stream.Stream;
public class StringToUpper {
public static void main(String args[]){
Stream.of("eeny ","meeny ",null).forEach(StringToUpper::toUpper);
}
private static void toUpper(String str) {
Optional <String> string = Optional.ofNullable(str);
System.out.print(string.map(String::toUpperCase).orElse("dummy"));
}
}
This program prints: EENY MEENY dummy This program prints: EENY MEENY DUMMY This program prints: EENY MEENY null This program prints: Optional[EENY] Optional[MEENY] Optional[dummy] This program prints: Optional[EENY] Optional[MEENY] Optional[DUMMY]
答案:
D. This code segment terminates normally without printing any output in the console. A stream pipeline is lazily evaluated. Since there is no terminal operation provided (such as count()
, forEach()
, reduce()
, or collect())
, this pipeline is not evaluated and hence the peek()
method does not print any output to the console. E. This program crashes by throwing java.lang.IllegalStateException
A stream once used–i.e., once “consumed”–cannot be used again. In this program, anyMatch()
is a terminal operation. Hence, once anyMatch()
is called, the stream in temperatures
is considered “used” or “consumed”. Hence, calling findAny()
terminal operation on temperatures
results in the program throwing java.lang.IllegalStateException
. F. This program prints: true The predicate str -> str.length() > 5
returns false
for all the elements because the length of each string is 2. Hence, the filter()
method results in an empty stream and the peek()
method does not print anything. The allMatch()
method returns true
for an empty stream and does not evalute the given predicate. Hence this program prints true
. C. This program prints: mo miny meeny eeny This is a proper definition of a lambda expression. Since the second argument of Collections.sort()
method takes the functional interface Comparator
and a matching lambda expression is passed in this code. Note that second argument is compared with the first argument in the lambda expression (str1, str2) -> str2.compareTo(str1)
. For this reason, the comparison is performed in descending order. A. Compiler error: Cannot find symbol “sum” in interface Stream<Integer>
Data and calculation methods such as sum()
and average()
are not available in the Stream<T>
interface; they are available only in the primitive type versions IntStream
, LongStream
, and DoubleStream
. To create an IntStream
, one solution is to use mapToInt()
method instead of map()
method in this program. If mapToInt()
were used, this program would compile without errors, and when executed, it will print 6 to the console. C. This program results in a compiler error in line marked with comment #3 The ifPresent()
method in Optional
takes a Consumer<T>
as the argument. This program uses ifPresent()
without passing an argument and hence it results in a compiler error. If the method isPresent()
were used instead of ifPresent()
in this program, it will compile cleanly and print 99 on the console. A. This program prints: EENY MEENY dummy Note that the variable string
points to Optional.ofNullable(str)
. When the element null is encountered in the stream, it cannot be converted to uppercase and hence the orElse()
method executes to return the string “dummy”. In this program, if Optional.of(str)
were used instead of Optional.ofNullable(str)
the program would have resulted in throwing a NullPointerException
.
七、异常和断言
认证目标 |
---|
使用 try-catch 和 throw 语句 |
使用 catch、multi-catch 和 finally 子句 |
将自动关闭资源与 try-with-resources 语句一起使用 |
创建自定义异常和可自动关闭的资源 |
使用断言测试不变量 |
在这一章中,你将详细了解 Java 对异常处理的支持。OCAJP 8 级考试(OCPJP 8 级考试的先决条件)将异常处理的基础知识作为考试主题。因此,我们假设您已经熟悉用于异常处理的基本语法和异常类型。在这一章中,你将学习如何提供 try、catch、multi-catch 和 finally block。您还将了解最近添加的语言特性,例如 try-with-resources 和 multi-catch 语句。接下来,您将学习如何定义自己的异常类(自定义异常)。最后,我们将讨论断言的相关主题,并教你如何在你的程序中使用它们。本章中的许多编程示例利用 I/O 函数(第九章和第十章)来说明异常处理的概念。
Throwable 及其子类
在 Java 中,抛出的对象应该是类Throwable
或其子类之一的实例(Throwable
是 Java 中异常层次结构的顶点类)。像throw
语句、throws
子句和catch
子句这样的异常处理构造只处理Throwable
及其子类。您需要详细了解Throwable
的三个重要子类:Error
、Exception
和RuntimeException
类。图 7-1 提供了这些类的高级概述。
图 7-1。
Java’s exception hierarchy
下面是对扩展了Throwable
类的三个重要类的简要描述:
- 类型为
Exception
的异常被称为检查异常。如果代码可以抛出一个Exception
,您必须使用 catch 块来处理它,或者声明该方法抛出该异常,强制该方法的调用者处理该异常。 RuntimeException
是Exception
类的派生类。从此类派生的异常称为未检查异常。处理未检查的异常是可选的。如果您在方法中编写的代码段会引发未检查的异常,则不一定要捕获该异常或在该方法的 throws 子句中声明该异常。- 当 JVM 在程序中检测到严重的异常情况时,它会引发类型为
Error
的异常。类型Error
的异常表示程序中的异常情况。捕捉这个异常并试图继续执行和假装什么都没发生是没有意义的。这样做实在是不好的做法!
现在,让我们开始讨论如何抛出和捕捉异常。
抛出异常
认证目标 |
---|
使用 try-catch 和 throw 语句 |
清单 7-1 是一个非常简单的编程示例,其中您希望将作为命令行参数键入的文本回显给用户。假设用户必须键入一些文本作为要回应的命令行参数,否则您需要通知用户“错误情况”
Listing 7-1. Echo.java
// A simple program without exception handling code
class Echo {
public static void main(String []args) {
if(args.length == 0) {
// no arguments passed – display an error to the user
System.out.println("Error: No input passed to echo command… ");
System.exit(-1);
}
else {
for(String str : args) {
// command-line arguments are separated and passed as an array
// print them by adding a space between the array elements
System.out.print(str + " ");
}
}
}
}
在这种情况下,您使用一个println()
语句在控制台中打印错误。这是一个简单的程序,错误发生在main()
方法中,所以错误处理很容易。在这种情况下,您可以在将错误信息打印到控制台后终止程序。然而,如果您在一个复杂的应用中深入函数调用,您需要一种更好的方法来指示“异常情况”已经发生,然后通知调用者该情况。此外,您通常需要从错误状态中恢复,而不是终止程序。因此,您需要能够“处理”一个异常,或者在调用堆栈中进一步“再抛出”该异常,以便调用者可以处理该异常。(我们将在本章的后面再讨论这个重新抛出异常的主题。)目前,您将修改清单 7-1 中的程序来抛出一个异常,而不是打印一条错误消息(在一个单独的程序Echo1.java)
中),如下所示:
if(args.length == 0) {
// no arguments passed - throw an exception
throw new IllegalArgumentException("No input passed to echo command");
}
args.length == 0
的if
条件内的这个程序块是该程序中唯一需要更改的部分。注意抛出异常的语法:关键字throw
后跟异常对象。这里您使用了已经在 Java 库中定义的IllegalArgumentException
。在本章的后面,你将看到如何定义你自己的异常。
现在,如果您在命令行中没有传递任何参数的情况下运行这个程序,程序将抛出一个IllegalArgumentException
:
Exception in thread "main" java.lang.IllegalArgumentException: No input passed to echo command
at Echo1.main(Echo1.java:5)
由于这个异常没有处理程序,这个未被捕获的异常终止了程序。在这种情况下,您显式地抛出了一个异常。当您编写一些代码或调用 Java APIs 时,也可能会抛出异常。现在我们来看一个例子。
未处理的异常
考虑清单 7-2 中的程序,它试图读取用户在控制台中键入的整数值,并将读取的整数打印回控制台。为了从控制台读取一个整数,您使用了在java.util.Scanner
类中提供的nextInt()
方法。要实例化Scanner
类,您需要传入System.in
,这是对系统输入流的引用。
Listing 7-2. ScanInt1.java
// A simple progam to accept an integer from user
import java.util.Scanner;
class ScanInt1 {
public static void main(String [] args) {
System.out.println("Type an integer in the console: ");
Scanner consoleScanner = new Scanner(System.in);
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
}
}
当您运行此程序并在控制台中键入一个整数,比如 10,程序会正常工作并成功地将该整数打印出来。
D:\> java ScanInt1
Type an integer in the console:
10
You typed the integer value: 10
但是,如果您(或程序的用户)错误地键入了字符串“ten”而不是整数值“10”怎么办?程序将在抛出如下异常后终止:
D:\> java ScanInt1
Type an integer in the console:
ten
Exception in thread "main" java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:909)
at java.util.Scanner.next(Scanner.java:1530)
at java.util.Scanner.nextInt(Scanner.java:2160)
at java.util.Scanner.nextInt(Scanner.java:2119)
at ScanInt.main(ScanInt1.java:7)
如果你读了nextInt()
的文档,你会看到这个方法可以抛出InputMismatchException
-"如果下一个令牌不匹配Integer
正则表达式,或者超出范围。"在这个简单的程序中,假设您(或用户)将总是按预期键入整数值,当假设失败时,将抛出一个异常。如果一个程序抛出了一个异常,并且它没有被处理,那么在抛出一个堆栈跟踪之后,程序将会异常终止,就像这里显示的那样。
堆栈跟踪显示在控件到达引发异常的语句之前调用的方法列表(带有行号)。作为一名程序员,您会发现跟踪控制流对于调试程序和修复导致该异常的问题非常有用。
那么,你如何处理这种情况呢?您需要将这段代码放在 try 和 catch 块中,然后处理异常。
Try 和 Catch 语句
认证目标 |
---|
使用 catch、multi-catch 和 finally 子句 |
Java 提供了try
和catch
关键字来处理您编写的代码中可能抛出的任何异常。清单 7-3 是清单 7-2 程序的改进版本。
Listing 7-3. ScanInt2.java
// A simple progam to accept an integer from user in normal case,
// otherwise prints an error message
import java.util.Scanner;
import java.util.InputMismatchException;
class ScanInt2 {
public static void main(String [] args) {
System.out.println("Type an integer in the console: ");
Scanner consoleScanner = new Scanner(System.in);
try {
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
} catch(InputMismatchException ime) {
// nextInt() throws InputMismatchException in case anything
// other than an integer is typed in the console; so handle it
System.out.println("Error: You typed some text that is not an integer value…");
}
}
}
如果在输入中键入了有效整数以外的任何内容,该程序会向用户打印一条可读的错误消息。
D:\> java ScanInt2
Type an integer in the console:
ten
Error: You typed some text that is not an integer value…
现在让我们来分析这段代码。后面跟有try
关键字的代码块限制了代码段,您预计可能会抛出一些异常。如果从try
块抛出任何异常,Java 运行时将搜索匹配的处理程序(稍后我们将更详细地讨论)。在这种情况下,出现了一个InputMismatchException
的异常处理程序,它与抛出的异常类型完全相同。这个完全匹配的 catch 处理程序可以在 try 块之外以关键字catch
开头的块的形式获得,并且这个 catch 块被执行。在 catch 块中,您捕获了异常,所以您在这里处理异常。你提供了一个人类可读的错误字符串,而不是抛出一个原始的堆栈跟踪(正如你在清单 7-2 中的早期程序中所做的),所以你为程序提供了一个优雅的退出。
多抓块
在清单 7-2 中,您使用了一个Scanner
对象从控制台读取一个整数。注意,你也可以使用一个Scanner
对象来读取一个字符串(参见清单 7-4 )。
Listing 7-4. ScanInt3.java
// A program that scans an integer from a given string
import java.util.Scanner;
import java.util.InputMismatchException;
class ScanInt3 {
public static void main(String [] args) {
String integerStr = "100";
System.out.println("The string to scan integer from it is: " + integerStr);
Scanner consoleScanner = new Scanner(integerStr);
try {
System.out.println("The integer value scanned from string is: " +
consoleScanner.nextInt());
} catch(InputMismatchException ime) {
// nextInt() throws InputMismatchException in case
// anything other than an integeris provided in the string
System.out.println("Error: Cannot scan an integer from the given string");
}
}
}
该程序打印以下内容:
The string to scan integer from it is: 100
The integer value scanned from string is: 100
如果您修改清单 7-4 中的程序,使字符串包含一个非整数值,如
String integerStr = "hundred";
try 块将抛出一个InputMismatchException
,它将在 catch 块中处理,您将得到以下输出:
The string to scan integer from it is: hundred
Error: Cannot scan an integer from the given string
现在,如果您修改清单 7-4 中的程序,使字符串包含一个空字符串,如
String integerStr = "";
为此,nextInt()
会抛出一个NoSuchElementException
,这个程序没有处理,所以这个程序会崩溃。
The string to scan integer from it is:
Exception in thread "main" java.util.NoSuchElementException
at java.util.Scanner.throwFor(Scanner.java:907)
at java.util.Scanner.next(Scanner.java:1530)
at java.util.Scanner.nextInt(Scanner.java:2160)
at java.util.Scanner.nextInt(Scanner.java:2119)
at ScanInt3.main(ScanInt.java:11)
此外,如果您查看 JavaDoc for Scanner.nextInt()
方法,您会发现它也可以抛出一个IllegalStateException
(如果在一个已经关闭的Scanner
对象上调用nextInt()
方法,就会抛出这个异常)。因此,让我们为InputMismatchException
、NoSuchElementException
和IllegalStateException
提供捕捉处理程序(参见清单 7-5 )。
Listing 7-5. ScanInt4.java
// A program that scans an integer from a given string
import java.util.Scanner;
import java.util.InputMismatchException;
import java.util.NoSuchElementException;
class ScanInt4 {
public static void main(String [] args) {
String integerStr = "";
System.out.println("The string to scan integer from it is: " + integerStr);
Scanner consoleScanner = new Scanner(integerStr);
try {
System.out.println("The integer value scanned from string is: " +
consoleScanner.nextInt());
} catch(InputMismatchException ime) {
System.out.println("Error: Cannot scan an integer from the given string");
} catch(NoSuchElementException nsee) {
System.out.println("Error: Cannot scan an integer from the given string");
} catch(IllegalStateException ise) {
System.out.println("Error: nextInt() called on a closed Scanner object");
}
}
}
以下是运行该程序时的输出:
The string to scan integer from it is:
Error: Cannot scan an integer from the given string
正如您在输出中看到的,由于字符串是空的,NoSuchElementException
被抛出。它在这个异常的 catch 处理程序中被捕获,catch 块中提供的代码被执行以导致一个优雅的退出。
请注意,您是如何通过堆叠它们来提供多个 catch 处理程序的:您提供了特定的(即派生类型)异常处理程序,然后是更通用的(即基本类型)异常处理程序。如果在基本异常类型之后提供派生异常类型,则会出现编译器错误。你可能还不知道,但是NoSuchElementException
是InputMismatchException
的基类!看看当您试图颠倒InputMismatchException
和NoSuchElementException
的 catch 处理程序的顺序时会发生什么。
try {
System.out.println("The integer value scanned from string is: "
+ consoleScanner.nextInt());
} catch(NoSuchElementException nsee) {
System.out.println("Error: Cannot scan an integer from the given string");
} catch(InputMismatchException ime) {
System.out.println("Error: Cannot scan an integer from the given string");
}
这段代码将导致以下编译器错误:
ScanInt4.java:14: error: exception InputMismatchException has already been caught
} catch(InputMismatchException ime) {
^
1 error
提供多个 catch 处理程序时,先处理特定异常,再处理一般异常。如果在基类异常处理程序之后提供派生类异常捕获处理程序,您的代码将不会编译。
多抓块
Java 提供了一个名为 multi-catch blocks 的特性,您可以在其中组合多个 catch 处理程序。让我们使用这个特性来组合NoSuchElementException
和IllegalStateException
的 catch 子句(参见清单 7-6 ):
Listing 7-6. ScanInt5.java
// A program that illustrates multi-catch blocks
import java.util.Scanner;
import java.util.NoSuchElementException;
class ScanInt5 {
public static void main(String [] args) {
String integerStr = "";
System.out.println("The string to scan integer from it is: " + integerStr);
Scanner consoleScanner = new Scanner(integerStr);
try {
System.out.println("The integer value scanned from string is: " +
consoleScanner.nextInt());
} catch(NoSuchElementException | IllegalStateException multie) {
System.out.println("Error: An error occured while attempting to scan the integer");
}
}
}
注意如何使用这里的| (OR)
操作符(对整数值执行逐位OR
操作的操作符)将 catch 处理程序组合在一起,以组合NoSuchElementException
和IllegalStateException
的 catch 子句。
与组合的NoSuchElementException
和IllegalStateException
的 catch 子句不同,您不能组合NoSuchElementException
和InputMismatchException
的 catch 子句。正如我们已经讨论过的,NoSuchElementException
是InputMismatchException
的基类,你不能在多重捕获块中同时捕获它们。如果您尝试编译这样一个多重捕获子句,您将得到以下编译器错误:
ScanInt5.java:11: error: Alternatives in a multi-catch statement cannot be related by subclassing
} catch(InputMismatchException | NoSuchElementException exception) {
^
那么还有什么选择呢?当您需要这样的异常捕获处理程序,其中一个异常是另一个异常类的基类时,只为基类提供捕获处理程序就足够了(因为基类捕获处理程序将在派生类异常发生时处理它)。
在 multi-catch 块中,不能为共享基类和派生类关系的两个异常组合 catch 处理程序。对于不共享父子关系的异常,只能组合 catch 处理程序。
如何知道合并异常处理块好还是堆叠好?这是一个设计选择,您必须考虑以下几个方面:(a)抛出异常的原因是相似的还是不同的?(b)处理代码相似还是不同?如果两个问题你都回答“差不多”,不如合并;如果你对这两个问题中的任何一个说“不一样”,那么还是分开来说比较好。
清单 7-6 具体情况如何?是组合还是分离InputMismatchException
和IllegalStateException
异常的处理程序更好?您可以看到两个 catch 块的异常处理是相同的。但是这两个异常的原因是相当不同的。InputMismatchException
抛出无效输入被传递(例如,我们前面讨论的“百”)。在调用了Scanner
上的close()
方法之后,当您调用nextInt()
方法时,IllegalStateException
会因为编程错误而被抛出。因此,在这种情况下,将这两个异常的处理程序分开是更好的设计选择。
一般捕获处理程序
您是否注意到,当您使用与 I/O 操作相关的 API 时,会抛出许多异常?我们刚刚讨论过,为了调用Scanner
类的一个方法nextInt()
,您需要处理三个异常:InputMismatchException
、NoSuchElementException
和IllegalStateException
。如果您继续处理像这样的特定异常,那么在您运行程序时,可能会也可能不会导致异常情况,您的大部分代码将由 try-catch 代码块组成!“处理所有其他异常”有没有更好的说法?是的,您可以提供一个通用的异常处理程序。
下面的代码片段只显示了清单 7-4 中的类ScanInt3
的 try-catch 块,增强了一个通用异常处理程序:
try {
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
} catch(InputMismatchException ime) {
// if something other than integer is typed, we'll get this exception, so handle it
System.out.println("Error: You typed some text that is not an integer value…");
} catch(Exception e) {
// catch IllegalStateException here which is unlikely to occur…
System.out.println("Error: Encountered an exception and could not read an integer from the console… ");
}
这段代码为类型Exception
的基本异常提供了一个 catch 处理程序。因此,如果 try 块抛出除了InputMismatchException
之外的任何异常,并且该异常是Exception
类的派生类,这个通用的 catch 处理程序将处理它。推荐的做法是捕捉特定的异常,然后提供一个通用的异常处理程序,以确保所有其他异常也得到处理。
释放资源
您是否注意到我们在清单 7-2 、 7-3 和 7-4 中讨论的程序有资源泄漏(因为我们打开了一个Scanner
对象但没有关闭它)?单词“resource”是指从底层操作系统获取一些系统资源的任何类,例如网络、文件、数据库和其他句柄。但是你怎么知道哪些课需要停课呢?答案是,如果一个类实现了java.io.Closeable
,那么你必须调用那个类的close()
方法;否则,将会导致资源泄漏。
垃圾收集器(GC)只负责释放内存资源。如果您正在使用任何获取系统资源的类,那么您有责任通过调用该对象上的
close()
方法来释放它们。
ScanInt6
(清单 7-7 )在其main()
方法中调用Scanner
对象的close()
方法;您希望缩短代码,所以您将使用一个通用的异常处理程序来处理所有可能在 try 块中抛出的异常。
Listing 7-7. ScanInt6.java
import java.util.Scanner;
class ScanInt6 {
public static void main(String [] args) {
System.out.println("Type an integer in the console: ");
Scanner consoleScanner = new Scanner(System.in);
try {
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
System.out.println("Done reading the text… closing the Scanner");
consoleScanner.close();
} catch(Exception e) {
// call all other exceptions here …
System.out.println("Error: Encountered an exception and could not read an integer from the console… ");
System.out.println("Exiting the program - restart and try the program again!");
}
}
}
让我们看看这个程序是否有效。
D:\> java ScanInt6
Type an integer in the console:
10
You typed the integer value: 10
Done reading the text… closing the Scanner
因为程序打印了"Done reading the text… closing the Scanner"
,并且正常完成了执行,所以可以假设语句consoleScanner.close()
;
已经执行成功。如果抛出异常会发生什么?
D:\> java ScanInt6
Type an integer in the console:
ten
Error: Encountered an exception and could not read an integer from the console…
Exiting the program - restart and try the program again!
从输出中可以看到,程序没有打印"Done reading the text… closing the Scanner"
,所以语句consoleScanner.close();
没有执行。你能如何修理它?一种方法是在 catch 块中调用consoleScanner.close()
,就像这样:
try {
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
System.out.println("Done reading the text… closing the Scanner");
consoleScanner.close();
} catch(Exception e) {
// call all other exceptions here …
consoleScanner.close();
System.out.println("Error: Encountered an exception and could not read an integer from the console… ");
System.out.println("Exiting the program - restart and try the program again!");
}
这种解决方案可行,但并不优雅。您知道您可以有多个 catch 块,并且您必须在所有 catch 块中提供对consoleScanner.close();
的调用!有没有更好的释放资源的方法?是的,你可以在一个finally
块中使用发布资源(参见清单 7-8 )。
Listing 7-8. ScanInt7.java
import java.util.Scanner;
class ScanInt7 {
public static void main(String [] args) {
System.out.println("Type an integer in the console: ");
Scanner consoleScanner = new Scanner(System.in);
try {
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
} catch(Exception e) {
// call all other exceptions here …
System.out.println("Error: Encountered an exception and could not read an integer from the console… ");
System.out.println("Exiting the program - restart and try the program again!");
} finally {
System.out.println("Done reading the integer… closing the Scanner");
consoleScanner.close();
}
}
}
在这种情况下,在 catch 块之后提供了一个finally
块。无论异常是否发生,这个 finally 块都将被执行。所以,finally
块是调用Scanner
对象上的close()
方法的好地方,以确保这个资源总是被释放。
如果你在一个方法内部调用
System.exit()
,它将异常终止程序。因此,如果调用方法有一个 finally 块,它将不会被调用,资源可能会泄漏。由于这个原因,调用System.exit()
来终止一个程序是一个糟糕的编程实践。
现在,让我们看看在程序正常完成(即没有抛出异常)和程序在抛出异常后终止的情况下,扫描器是否关闭。
D:\> java ScanInt7
Type an integer in the console:
10
You typed the integer value: 10
Done reading the integer… closing the Scanner
D:\> java ScanInt7
Type an integer in the console:
ten
Error: Encountered an exception and could not read an integer from the console…
Exiting the program - restart and try the program again!
Done reading the integer… closing the Scanner
是的,不管是否抛出异常,都会调用语句"Done reading the integer… closing the Scanner"
。请注意,您可以在没有catch
块的情况下,在try
块之后直接有一个finally
块;虽然这个特性很少使用,但它是一个有用的特性。
注意:finally
块总是被执行,不管try
块中的代码是否抛出异常。考虑下面的方法。它会向调用者返回 true 还是 false?
boolean returnTest() {
try {
return true;
}
finally {
return false;
}
}
这个方法将总是返回 false,因为finally
总是被调用,尽管它是不直观的。事实上,如果您使用"-Xlint"
选项,您将得到这个编译器警告:“finally 子句不能正常完成。”(注意,你可以有一个try
块,后面跟着catch
块或finally
块,或者两个块都跟着。)
抛出条款
方法可以抛出检查过的异常;子句 throw 在方法签名中指定了这些检查过的异常。在throws
子句中,您列出了一个方法可能抛出的检查过的异常。为什么我们需要抛出条款?通过查看 throws 子句,您可以清楚地了解该方法可以抛出什么异常。理解检查异常是理解 throws 子句的先决条件。由于我们已经在前一节异常类型中讨论了检查异常,现在我们将讨论 throws 子句。
让我们试着读取当前目录中名为integer.txt
的文件中存储的一个整数。有一个Scanner
类的重载构造函数,它接受一个File
对象作为输入,所以让我们试着使用它。清单 7-9 显示了程序。有用吗?
Listing 7-9. ThrowsClause1.java
import java.io.File;
import java.util.Scanner;
class ThrowsClause1 {
public static void main(String []args) {
System.out.println("Reading an integer from the file 'integer.txt': ");
Scanner consoleScanner = new Scanner(new File("integer.txt"));
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
}
}
这段代码将导致编译器错误"unreported exception FileNotFoundException; must be caught or declared to be thrown"
。如果您查看这个Scanner
方法的声明,您会看到一个 throws 子句:
public Scanner(File source) throws FileNotFoundException {
因此,任何调用这个构造函数的方法都应该要么处理这个异常,要么添加一个 throws 子句来声明该方法可以抛出这个异常。向main()
方法添加一个 throws 子句;参见清单 7-10 。
Listing 7-10. ThrowsClause2.java
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
class ThrowsClause2 {
public static void main(String []args) throws FileNotFoundException {
System.out.println("Reading an integer from the file 'integer.txt': ");
Scanner consoleScanner = new Scanner(new File("integer.txt"));
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
}
}
如果您运行这个程序,并且没有名为integer.txt
的文件,程序将在抛出这个异常后崩溃:
Reading an integer from the file 'integer.txt':
Exception in thread "main" java.io.FileNotFoundException: integer.txt (The system cannot find the file specified)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.util.Scanner.<init>(Scanner.java:656)
at ThrowsClause2.main(ThrowsClause2.java:7)
现在让我们将main()
方法中的代码提取到一个名为readIntFromFile()
的新方法中。您已经将它定义为一个实例方法,所以您还创建了一个ThrowsClause3
类的对象来从main()
方法中调用这个方法。由于readIntFromFile()
内部的代码可以抛出一个FileNotFoundException
,它必须要么引入一个 catch 处理程序来处理这个异常,要么在其 throws 子句中声明这个异常(参见清单 7-11 )。
Listing 7-11. ThrowsClause3.java
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
class ThrowsClause3 {
// since this method does not handle FileNotFoundException,
// the method must declare this exception in the throws clause
public int readIntFromFile() throws FileNotFoundException {
Scanner consoleScanner = new Scanner(new File("integer.txt"));
return consoleScanner.nextInt();
}
// since readIntFromFile() throws FileNotFoundException and main() does not handle
// it, the main() method declares this exception in its throws clause
public static void main(String []args) throws FileNotFoundException {
System.out.println("Reading an integer from the file 'integer.txt': ");
System.out.println("You typed the integer value: "
+ new ThrowsClause3().readIntFromFile());
}
}
在清单 7-10 和 7-11 中,程序的行为保持不变。然而,清单 7-11 显示了main()
方法也必须声明如何在其 throws 子句中抛出FileNotFoundException
(否则,程序将无法编译)。
方法重写和 Throws 子句
当一个可重写的方法有一个 throws 子句时,在重写该方法时有许多事情要考虑。考虑清单 7-12 中的程序,它实现了一个名为IntReader
的接口。这个接口声明了一个名为readIntFromFile()
的方法,其中的 throws 子句列出了一个FileNotFoundException
。
Listing 7-12. ThrowsClause4.java
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Scanner;
// This interface is meant for implemented by classes that would read an integer from a file
interface IntReader {
int readIntFromFile() throws IOException;
}
class ThrowsClause4 implements IntReader {
// implement readIntFromFile with the same throws clause
// or a more specific throws clause
public int readIntFromFile() throws FileNotFoundException {
Scanner consoleScanner = new Scanner(new File("integer.txt"));
return consoleScanner.nextInt();
}
// main method elided in this code since the focus here is to understand
// issues related to overriding when throws clause is present
}
在这段代码中,您可以观察到一些重要的事实:
-
可以为接口中声明的方法声明 throws 子句;事实上,您也可以为在抽象类中声明的抽象方法提供 throws 子句。
-
The method declared in the
IntReader
interface declares to throwIOException
, which is a more general exception than aFileNotFoundException
(Figure 7-2). While implementing a method, it is acceptable to either provide the throws clause listing the same exception type as the base method or a more specific type than the base method. In this case, thereadIntFromFile()
method lists a more specific exception (FileNotFoundException
) in its throws clause against the more general exception ofIOException
listed in the throws clause of the base method declared in theIntReader
interface.图 7-2。
Class hierarchy of FileNotFoundException
如果你试着改变 throws 条款呢?有许多方法可以更改 overriding 方法中的 throws 子句,包括:
Listing more general checked exceptions to throw. Listing more checked exceptions in addition to the given checked exception(s) in the base method.
如果您尝试这些情况中的任何一种,您都会得到一个编译器错误。例如,如果提供比基类中指定的更一般的异常,将导致编译器错误。
如果被重写的方法不抛出任何已检查的异常,或者如果抛出,它提供一个 try-catch 块,则可以选择不在被重写的方法中使用 throws 子句指定任何异常。
总而言之,基类方法的 throws 子句是它提供给该方法的调用方的契约:它规定调用方应该处理列出的异常或在其 throws 子句中声明这些异常。当重写基方法时,派生方法也应该遵守该协定。基方法的调用方只准备处理基方法中列出的异常,因此重写方法不能引发更一般的异常或除列出的已检查异常之外的异常。
但是,请注意,关于派生类方法的 throws 子句应该遵循基方法的 throws 子句的约定的讨论仅限于检查异常。与基类方法的 throws 子句相比,仍可以在协定中添加或移除未检查的异常。例如,考虑以下情况:
public int readIntFromFile() throws IOException, NoSuchElementException {
Scanner consoleScanner = new Scanner(new File("integer.txt"));
return consoleScanner.nextInt();
}
这是一个可接受的 throws 子句,因为NoSuchElementException
可以从readIntFromFile()
方法中抛出。这个异常是一个未检查的异常,当nextInt()
方法不能从文件中读取整数时抛出。这是一种常见的情况,例如,如果您有一个名为integer.txt;
的空文件,试图从该文件中读取一个整数将导致这个异常。
@Throws Tag
使用@throws
JavaDoc 标签(或者它的同义词@exception
标签)来记录一个方法可能抛出(未检查的或者检查的)异常的具体情况或者情况是一个好的实践。下面是提供@throws
标签的格式和一个例子:
@throws exception-name description-text
@throws IllegalStateException if this scanner is closed
此标记只能用于方法和构造函数。
下面是Scanner
类中nextInt()
方法的 JavaDoc 注释示例:
/**
* Scans the next token of the input as an <tt>int</tt>.
*
* <p> An invocation of this method of the form
* <tt>nextInt()</tt> behaves in exactly the same way as the
* invocation <tt>nextInt(radix)</tt>, where <code>radix</code>
* is the default radix of this scanner.
*
* @return the <tt>int</tt> scanned from the input
*
@throws InputMismatchException
* if the next token does not match the <i>Integer</i>
* regular expression, or is out of range
*``@throws NoSuchElementException
*``@throws``IllegalStateException
*/
public int nextInt() {
return nextInt(defaultRadix);
}
注意InputMismatchException
、NoSuchElementException
和IllegalStateException
的@throws
标签。当一个方法可以抛出多个异常时,它们按照惯例按字母顺序列出(就像本例中一样)。
要记住的要点
以下是关于 throws 语句的一些值得注意的要点,可能对你参加 OCPJP 八级考试有所帮助:
- 如果一个方法没有 throws 子句,并不意味着它不能抛出任何异常;这只是意味着它不能抛出任何检查过的异常。
- 静态初始化块不能抛出任何已检查的异常。为什么呢?请记住,静态初始化块是在加载类时调用的,因此没有办法处理调用程序中抛出的异常。此外,没有办法在 throws 子句中声明被检查的异常(因为它们是块,而不是方法)。
- 非静态初始化块可能会引发检查过的异常;然而,所有的构造函数都应该在它们的 throws 子句中声明这些异常。为什么呢?编译器在其代码生成阶段合并非静态初始化块和构造函数的代码,因此构造函数的 throws 子句可用于声明非静态初始化块可能引发的检查异常。
- 重写方法在 throws 子句中声明的检查异常不能多于基方法的 throws 子句中声明的异常列表。为什么呢?基方法的调用方只能看到该方法的 throws 子句中给出的异常列表,并将在其代码中声明或处理这些检查过的异常(仅此而已)。
- 重写方法可以声明比基方法的 throws 子句中列出的异常更具体的异常;换句话说,您可以在重写方法的 throws 子句中声明派生异常。
- 如果一个方法在两个或多个接口中声明,并且如果该方法在 throws 子句中声明抛出不同的异常,则该方法实现应该列出所有这些异常。
链接和重新引发异常
您可以捕捉异常,将它们包装成更一般的异常,并在调用堆栈中将其抛出。当您捕获一个异常并创建一个更一般的异常时,您可以保留对原始异常的引用;这被称为异常链接。
catch(LowLevelException lle) {
// wrap the low-level exception to a higher-level exception;
// also, chain the original exception to the newly thrown exception
throw new HighLevelException(lle);
}
链接异常对于调试非常有用。当您得到一个一般的异常时,您可以检查是否有一个连锁的较低级别的异常,并尝试理解为什么会发生那个较低级别的异常。
用资源尝试
认证目标 |
---|
将自动关闭资源与 try-with-resources 语句一起使用 |
Java 程序员很容易忘记释放资源,即使是在 finally 块中。此外,如果您正在处理多个资源,记住在 finally 块中调用close()
方法是很繁琐的。Try-with-resources 特性(在 Java 7 中引入)将有助于简化您的工作。清单 7-13 利用了这一特点;它是清单 7-8 的改进版本,清单 7-8 明确地调用了 close,如在consoleScanner.close()
中。
Listing 7-13. TryWithResources1.java
import java.util.Scanner;
class TryWithResources1 {
public static void main(String [] args) {
System.out.println("Type an integer in the console: ");
try(Scanner consoleScanner = new Scanner(System.in)) {
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
} catch(Exception e) {
// catch all other exceptions here …
System.out.println("Error: Encountered an exception and could not read an integer from the console… ");
System.out.println("Exiting the program - restart and try the program again!");
}
}
}
请务必仔细查看 try-with-resources 块的语法。
try(Scanner consoleScanner = new Scanner(System.in)) {
在这个语句中,您已经获得了在try
关键字之后,但是在try
块之前的括号内的资源。此外,在示例中,您没有提供 finally 块。Java 编译器会在内部将这个 try-with-resources 块翻译成 try-finally 块(当然,编译器会保留您提供的 catch 块)。您可以在 try-with-resources 块中获取多个资源。这种资源获取语句必须用分号分隔。
您能提供不带任何显式 catch 或 finally 块的 try-with-resources 语句吗?没错。请记住,try 块可以与 catch 块、finally 块或两者都关联。try-with-resources 语句块在内部扩展为 try-finally 块。因此,您可以提供一个没有显式 catch 或 finally 块的 try-with-resources 语句。清单 7-14 使用了一个没有任何显式 catch 或 finally 块的 try-with-resources 语句。
Listing 7-14. TryWithResources2.java
import java.util.Scanner;
class TryWithResources2 {
public static void main(String [] args) {
System.out.println("Type an integer in the console: ");
try(Scanner consoleScanner = new Scanner(System.in)) {
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
}
}
}
尽管可以创建一个没有任何显式 catch 或 finally 的 try-with-resources 语句,但这并不意味着您应该这样做!例如,由于这段代码没有 catch 块,如果您键入一些无效的输入,程序将会崩溃。
D:\> java TryWithResources2
Type an integer in the console:
ten
Exception in thread "main" java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:864)
at java.util.Scanner.next(Scanner.java:1485)
at java.util.Scanner.nextInt(Scanner.java:2117)
at java.util.Scanner.nextInt(Scanner.java:2076)
at TryWithResources2.main(TryWithResources2.java:7)
因此,try-with-resources 语句的好处是,它不必显式地提供 finally 块,从而简化了您的工作。但是,您仍然需要提供必要的 catch 块。
注意,对于可用于 try-with-resources 语句的资源,该资源的类必须实现java.lang.AutoCloseable
接口。这个接口声明了一个名为close()
的方法。除了资源尝试特性,AutoCloseable
接口也被引入到 Java 7 中,该接口由Closeable
接口的基础接口组成。这是为了确保现有的资源类与 try-with-resources 语句无缝协作。换句话说,您可以将所有旧的流类与 try-with-resources 一起使用,因为它们实现了AutoCloseable
接口。
图 7-3。
Closeabe Interface extends AutoCloseable Interface
关闭多个资源
在 try-with-resources 语句中可以使用多个资源。下面是利用 try-with-resources 语句从给定的文本文件创建 zip 文件的代码片段:
// buffer is the temporary byte buffer used for copying data
// from one stream to another stream
byte [] buffer = new byte[1024];
// these stream constructors can throw FileNotFoundException
try (ZipOutputStream zipFile = new ZipOutputStream(new FileOutputStream(zipFileName));
FileInputStream fileIn = new FileInputStream(fileName)) {
zipFile.putNextEntry(new ZipEntry(fileName)); // putNextEntry can throw
// IOException
int lenRead = 0; // the variable to keep track of number of bytes sucessfully read
// copy the contents of the input file into the zip file
while((lenRead = fileIn.read(buffer)) > 0) { // read can throw IOException
zipFile.write(buffer, 0, lenRead); // write can throw IOException
}
// the streams will be closed automatically because they are within try-with-
// resources statement
}
在这段代码中,buffer
是一个字节数组。该数组是临时存储,用于将原始数据从一个流复制到另一个流。在 try-with-resources 语句中,您打开了两个流:ZipOutputStream
用于写入 zip 文件,而FileInputStream
用于读取文本文件。(注意:在java.util.zip
包中提供了对 zip(和 jar)文件的 API 支持。)您希望读取输入文本文件,将其压缩,并将条目放入压缩文件中。为了将文件/目录条目放入 zip 文件,ZipOutputStream
类提供了一个名为putNextEntry()
的方法,该方法将一个ZipEntry
对象作为参数。语句zipFile.putNextEntry(new ZipEntry(fileName));
将名为fileName
的文件条目放入zipFile
中。
为了读取文本文件的内容,可以使用FileInputStream
类中的read()
方法。read()
方法将buffer
数组作为参数。每次迭代要读取的数据量(即要读取的“数据块大小”)由传递的数组的大小给出;这段代码是 1024 字节。read()
方法返回它读取的字节数,如果没有更多的数据要读取,它返回-1。在写入 zip 文件之前,while
循环检查读取是否成功(使用> 0 条件)。
为了将数据写入 zip 文件,可以使用ZipOutputStream
类中的write()
方法。write()
方法有三个参数:第一个参数是数据缓冲区;第二个参数是数据缓冲区中的起始偏移量(它是 0,因为您总是从缓冲区的起始处读取);第三个是要写入的字节数。
现在我们进入主要讨论。请注意如何在 try 块中打开两个资源,并用分号分隔这两个资源获取语句。您没有显式的 finally 块来释放资源,因为编译器会自动在 finally 块中插入对这两个流的close
方法的调用。
清单 7-15 是一个完整的程序,它利用这段代码来说明如何使用 try-with-resources 语句来自动关闭多个流。
Listing 7-15. ZipTextFile.java
import java.util.*;
import java.util.zip.*;
import java.io.*;
// class ZipTextFile takes the name of a text file as input and creates a zip file
// after compressing that text file.
class ZipTextFile {
public static final int CHUNK = 1024; // to help copy chunks of 1KB
public static void main(String []args) {
if(args.length == 0) {
System.out.println("Pass the name of the file in the current directory to be zipped as an argument");
System.exit(-1);
}
String fileName = args[0];
// name of the zip file is the input file name with the suffix ".zip"
String zipFileName = fileName + ".zip";
byte [] buffer = new byte[CHUNK];
// these constructors can throw FileNotFoundException
try (ZipOutputStream zipFile = new ZipOutputStream(new FileOutputStream(zipFileName));
FileInputStream fileIn = new FileInputStream(fileName)) {
// putNextEntry can throw IOException
zipFile.putNextEntry(new ZipEntry(fileName));
int lenRead = 0; // variable to keep track of number of bytes
// successfully read
// copy the contents of the input file into the zip file
while((lenRead = fileIn.read(buffer)) > 0) {
// both read and write methods can throw IOException
zipFile.write (buffer, 0, lenRead);
}
// the streams will be closed automatically because they are
// within try-with-resources statement
}
// this can result in multiple exceptions thrown from the try block;
// use "suppressed exceptions" to get the exceptions that were suppressed!
catch(Exception e) {
System.out.println("The caught exception is: " + e);
System.out.print("The suppressed exceptions are: ");
for(Throwable suppressed : e.getSuppressed()) {
System.out.println(suppressed);
}
}
}
}
我们已经讨论过 try-with-resources 块。我们没有讨论的是被抑制的异常。在 try-with-resources 语句中,可能会抛出多个异常;例如,一个在 try 块中,一个在 catch 块中,另一个在 finally 块中。但是,只能捕获一个异常,因此其他异常将被列为隐藏的异常。从一个给定的异常对象中,您可以使用方法getSuppressed()
来获得被抑制的异常的列表。
要记住的要点
以下是一些关于“用资源尝试”的有趣观点,将对你参加 OCPJP 八级考试有所帮助:
- 不能在 try-with-resources 语句体中为 try-with-resources 中声明的资源变量赋值。这是为了确保在 try-with-resources 头中获得的相同资源在 finally 块中被释放。
- 在 try-with-resources 语句中显式关闭资源是一个常见的错误。请记住,try-with-resources 扩展到调用 finally 块中的
close()
方法,因此如果您显式提供一个close()
方法,扩展后的代码将会对close()
方法进行两次调用。考虑以下代码:try(Scanner consoleScanner = new Scanner(System.in)) {
System.out.println("You typed the integer value: " + consoleScanner.nextInt());
consoleScanner.close();
// explicit call to close() method - remember that try-with-resources
// statement will also expand to calling close() in finally method;
// hence this will result in call to close() method in Scanner twice!
}
Scanner
类中的close()
方法的文档说明,如果 scanner 对象已经关闭,那么再次调用该方法将没有任何效果。所以,在这种情况下你是安全的。然而,一般来说,你不能期望所有的资源都实现了一个可以安全调用两次的close()
方法。因此,在 try-with-resource 语句中显式调用close()
方法是一种不好的做法。
自定义异常
认证目标 |
---|
创建自定义异常和可自动关闭的资源 |
在大多数情况下,抛出 Java 库中已经提供的异常就足够了。例如,如果您正在检查传递给一个公共函数的参数的有效性,并且发现它们为空或者超出了预期的范围,那么您可以抛出一个IllegalArgumentException
。然而,对于大多数重要的应用,您有必要开发自己的异常类(自定义异常)来指示异常情况。
如何定义自定义异常?有两个选项:您可以根据需要扩展Exception
或RuntimeException
类。
如果您想要强制您的自定义异常的用户处理该异常,那么您可以从Exception
类扩展您的异常类——这将使您的自定义异常成为一个检查异常。
如果你想给你的自定义异常的用户一些灵活性,并让你的异常的用户来决定他们是否要处理这个异常,你可以从RuntimeException
类中派生出你的异常。
因此,您需要决定是通过从Exception
类还是RuntimeException
类扩展来使您的自定义异常成为检查异常还是未检查异常。
为自定义异常扩展Throwable
或Error
类怎么样?Throwable
类太普通了,不能作为你的异常的基类,所以不推荐。Error
类是为 JVM 可能抛出的致命异常(如StackOverflowError
)保留的,所以不建议将它作为异常的基类。
自定义异常应该扩展
Exception
或RuntimeException
类。通过扩展Throwable
或Error
类来创建定制异常是一种不好的做法。
对于从基类扩展,您需要了解基类提供了哪些方法。在这种情况下,您希望通过扩展Exception
或RuntimeException
类来创建一个定制异常。由于Exception
类是RuntimeException
类的基类,所以知道Exception
类的成员就足够了。表 7-1 列出了Exception
类的重要方法(包括构造函数)。
表 7-1。
Important Methods and Constructors of the Exception Class
成员 | 简短描述 |
---|---|
Exception() |
Exception 类的默认构造函数,没有关于异常的附加(或详细)信息。 |
Exception(String) |
构造函数,它将有关构造函数的详细信息字符串作为参数。 |
Exception(String, Throwable) |
除了作为参数的详细信息字符串之外,这个异常构造函数还将异常的原因(这是另一个异常)作为参数。 |
Exception(Throwable) |
将异常原因作为参数的构造函数。 |
String getMessage() |
返回详细消息(在创建异常时作为字符串传递)。 |
Throwable getCause() |
返回异常的原因(如果有,否则返回 null)。 |
Throwable[] getSuppressed() |
以数组形式返回隐藏的异常(通常在使用 try-with-resources 语句时导致)的列表。 |
void printStackTrace() |
将堆栈跟踪(即带有相关行号的方法调用列表)打印到控制台(标准错误流)。如果异常的原因(是另一个异常对象)在异常中可用,那么也将打印该信息。此外,如果有任何隐藏的异常,也会打印出来。 |
为了演示如何创建自己的异常类,假设您想要创建一个名为InvalidInputException
的定制异常。当您试图读取输入(在本例中是读取一个整数)时,如果它失败了,您想抛出这个InvalidInputException
。清单 7-16 通过扩展RuntimeException
类定义了这个异常类。
Listing 7-16. InvalidInputException.java
// a custom "unchecked exception" that is meant to be thrown
// when the input provided by the user is invalid
class InvalidInputException extends RuntimeException {
// default constructor
public InvalidInputException() {
super();
}
// constructor that takes the String detailed information we pass while
// raising an exception
public InvalidInputException(String str) {
super(str);
}
// constructor that remembers the cause of the exception and
// throws the new exception
public InvalidInputException(Throwable originalException) {
super(originalException);
}
// first argument takes detailed information string created while
// raising an exception
// and the second argument is to remember the cause of the exception
public InvalidInputException(String str, Throwable originalException) {
super(str, originalException);
}
}
在这个InvalidInputException
类中,您没有引入任何新的字段,但是如果需要,您可以添加任何字段。这也是一个简单的自定义异常,其中构造函数简单地调用同一构造函数类型的基类版本。类CustomExceptionTest
(见清单 7-17 )展示了如何利用这个自定义异常。
Listing 7-17. CustomExceptionTest.java
import java.util.Scanner;
import java.util.NoSuchElementException;
// class for testing the custom exception InvalidInputException
class CustomExceptionTest {
public static int readIntFromConsole() {
Scanner consoleScanner = new Scanner(System.in);
int typedInt = 0;
try {
typedInt = consoleScanner.nextInt();
} catch(NoSuchElementException nsee) {
System.out.println("Wrapping up the exception and throwing it…");
throw new InvalidInputException("Invalid integer input typed in console", nsee);
} catch(Exception e) {
// call all other exceptions here …
System.out.println("Error: Encountered an exception and could not read an integer from the console… ");
}
return typedInt;
}
public static void main(String [] args) {
System.out.println("Type an integer in the console: ");
try {
System.out.println("You typed the integer value: " + readIntFromConsole());
} catch(InvalidInputException iie) {
System.out.println("Error: Invalid input in console… ");
System.out.println("The current caught exception is of type: " + iie);
System.out.println("The originally caught exception is of type: " +
iie.getCause());
}
}
}
在阅读代码的讨论之前,先编译并运行这个程序。
D:\> java CustomExceptionTest
Type an integer in the console:
one
Wrapping up the exception and throwing it…
Error: Invalid input in console…
The current caught exception is of type: InvalidInputException: Invalid integer input typed in console
The originally caught exception is of type: java.util.InputMismatchException
在这段代码中,就像 Java 库中已经定义的任何其他异常一样使用InvalidInputException
。您正在捕捉从main()
方法中的readIntFromConsole()
方法抛出的InvalidInputException InputMismatchException (which extends InvalidInputException
(为其提供了捕捉处理程序)。下面的语句调用了InvalidInputException
的toString()
方法:
System.out.println("The current caught exception is of type: " + iie);
您没有覆盖toString()
方法,所以InvalidInputException
类从RuntimeException
基类继承了toString()
方法的默认实现。这个默认的toString()
方法打印抛出的异常的名称(InvalidInputException
),它还包括创建异常对象时传递的详细信息字符串("Invalid integer input typed in console"
)。main()
方法中的最后一条语句是获取异常的原因。
System.out.println("The originally caught exception is of type: " + iie.getCause());
由于InvalidInputException
的原因是InputMismatchException
,这个异常名作为全限定名java.util.InputMismatchException
打印在控制台上。你可以认为InputMismatchException
导致InvalidInputException;
这两个异常被称为链式异常。
断言
认证目标 |
---|
使用断言测试不变量 |
在创建应用时,你会假设很多事情。然而,经常发生的是假设不成立,导致错误的条件。assert
语句用于检查或测试你对程序的假设。
关键字assert
为 Java 中的断言提供支持。每个断言语句都包含一个布尔表达式。如果布尔表达式的结果为真,就意味着假设为真,所以什么都不会发生。然而,如果布尔结果为假,那么您对程序的假设不再成立,并且抛出一个AssertionError
。记住Error
类和它的派生类指出了严重的运行时错误,不应该被处理。同样,如果抛出一个AssertionError
,最好的做法是不要捕捉异常,让程序终止。之后,您需要检查为什么假设不成立,然后修复程序。
有许多原因使你应该在程序中添加断言。一个原因是它有助于及早发现问题;当你在程序中检查你的假设时,当任何一个假设失败时,你立即知道在哪里寻找问题和修复什么。此外,当其他程序员阅读您的带有断言的代码时,他们会更好地理解代码,因为您使用断言来明确您的假设。
断言语句
Java 中的断言语句有两种形式:
assert booleanExpression;
assert booleanExpression : "Detailed error message string";
如果在 assert 语句中使用了非布尔表达式,则会出现编译器错误。清单 7-18 包含断言的第一个例子。
Listing 7-18. AssertionExample1.java
class AssertionExample1 {
public static void main(String []args) {
int i = -10;
if(i < 0) {
// if negative value, convert into positive value
i = -i;
}
System.out.println("the value of i is: " + i);
// at this point the assumption is that i cannot be negative;
// assert this condition since its an assumption that will always hold
assert (i >= 0) : "impossible: i is negative!";
}
}
在这个程序中,您正在检查 I 的值是否< 0;您正在使用表达式–I 将其转换为正值。一旦条件检查 if(i < 0)完成,I 的值不能为负,或者那是你的假设。这样的假设可以用 assert 语句来断言。下面是 assert 语句:
assert (i >= 0) : "impossible: i is negative!";
如果布尔表达式(i >= 0)的计算结果为真,程序将正常运行。但是,如果计算结果为 false,程序将通过抛出一个AssertionError
而崩溃。让我们检查一下这种行为(您需要使用–ea
标志在运行时启用断言;我们一会儿将讨论更多关于这个标志的内容)。
D:\>java -ea AssertionExample1
the value of i is: 10
是的,这个程序成功执行,没有抛出任何异常。
有没有一个 I 值使条件不成立?有,有!如果 I 的值是整数的最小可能值,那么它不能被转换成正值。为什么?记住整数的范围是-2 31 到 231–1,所以整数值 I 的值为-2147483648 到 2147483647。换句话说,正值 2147483648 不在整数范围内。因此,如果i
的值是-2147483648,那么表达式-i
将溢出并再次得到值-2147483648。因此,你的假设是不正确的。
在清单 7-26 中,将 I 的值改为一个整数的最小值,如下所示:
int i = Integer.MIN_VALUE;
现在,试着运行这个程序。
D:\> java -ea AssertionExample1
the value of i is: -2147483648
Exception in thread "main" java.lang.AssertionError: impossible: i is negative!
at AssertionExample1.main(AssertionExample1.java:12)
在这个输出中,注意断言是如何失败的。应用崩溃是因为程序抛出了AssertionError
,而且没有处理程序,所以程序终止。
从考试的角度来看,需要记住的重要一点是,断言在运行时是默认禁用的;要在运行时启用断言,请使用an -ea
开关(或其更长形式的-enableasserts
)。要在运行时禁用断言,请使用一个-da
开关。如果运行时默认禁用断言,那么-da
开关有什么用?有很多用途。例如,如果你想为一个给定的包中的所有类启用断言,并想禁用该包中特定类的断言,那么一个-da
开关就很有用。表 7-2 列出了重要的命令行参数及其含义。请注意,您不需要重新编译程序来启用或禁用断言;调用 JVM 时,只需使用命令行参数来启用或禁用它们。
表 7-2。
Important Command-Line Arguments for Enabling/Disabling Assertions
命令行参数 | 简短描述 |
---|---|
-ea |
默认情况下启用断言(系统类除外)。 |
-ea:<class name> |
为给定的类名启用断言。 |
-ea:<package name>… |
在给定包 |
-ea:… |
在给定的未命名包中启用断言。 |
-esa |
-enablesystemsassertions 的简称;在系统类中启用断言。这个选项很少使用。 |
-da |
默认情况下禁用断言(系统类除外)。 |
-da:<class name> |
禁用给定类名的断言。 |
-da:<package name>… |
禁用给定包 |
-da:… |
禁用给定未命名包中的断言。 |
-dsa |
-disablesystemsassertions 的简称;禁用系统类中的断言。这个选项很少使用。 |
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
使用 try-catch 和 throw 语句
- 当 try 块抛出异常时,JVM 从方法调用链中的 catch 处理程序列表中寻找匹配的 catch 处理程序。如果没有找到匹配的处理程序,该未处理的异常将导致应用崩溃。
- 在提供多个异常处理程序(堆栈式 catch 处理程序)时,应该在提供通用异常处理程序之前提供特定的异常处理程序。
- 您可以使用
printStackTrace()
和getStackTrace()
等方法以编程方式访问堆栈跟踪,这些方法可以在任何异常对象上调用。
使用 catch、multi-catch 和 finally 子句
- 一个 try 块可以有多个 catch 处理程序。如果两个或多个异常的原因相似,处理代码也相似,可以考虑合并处理程序,使之成为一个 multi-catch 块。
- catch 块应该处理异常或重新引发异常。通过捕捉异常而什么都不做来隐藏或吞下异常,这确实是一种不好的做法。
- 您可以包装一个异常,并将其作为另一个异常抛出。这两个异常成为链式异常。从抛出的异常中,可以得到异常的原因。
- 无论 try 块是成功执行还是导致异常,finally 块中的代码都将被执行。
将自动关闭资源与 try-with-resources 语句一起使用
- 忘记通过显式调用
close()
方法来释放资源是一个常见的错误。您可以使用 try-with-resources 语句来简化代码并自动关闭资源。 - 您可以在 try-with-resources 语句中自动关闭多个资源。这些资源需要在 try-with-resources 语句头中用分号分隔。
- 如果 try 块抛出异常,而 finally 块也抛出异常,那么 finally 块中抛出的异常将作为隐藏异常添加到从 try 块中抛出的异常中,并传递给调用方。
创建自定义异常和可自动关闭的资源
- 建议您从
Exception
或RuntimeException
类中派生自定义异常。 - 方法的 throws 子句是其派生类中的重写方法应该遵守的契约的一部分。
- 重写方法可以提供与基方法的 throws 子句相同的 throw 子句,或者提供比基方法的 throws 子句更具体的 throws 子句。
- 与基方法的 throws 子句相比,重写方法不能提供更通用的 throws 子句,也不能声明引发附加的检查异常。
- 对于在 try-with-resources 语句中可用的资源,该资源的类必须实现
java.lang.AutoCloseable
接口并定义close()
方法。
使用断言测试不变量
- 断言是程序中的条件检查,应该用于显式检查编写程序时所做的假设。
assert
语句有两种形式:一种采用一个Boolean
参数,另一种采用一个额外的字符串参数。- 如果 assert 参数中给出的
Boolean
条件失败(即评估为 false),程序将在抛出AssertionError
后终止。当程序抛出一个AssertionError
时,捕捉并恢复是不可取的。 - 默认情况下,断言在运行时是禁用的。在调用 JVM 时,可以使用命令行参数
–ea
(用于启用断言)和–da
(用于禁用断言)及其变体。
Question Time!Consider the following class hierarchy from the package java.nio.file
and answer the question.
In the following class definitions, the base class Base
has the method foo()
that throws a FileSystemException
; the derived class Deri
extending the class Base
overrides the foo()
definition. class Base {
public void foo() throws FileSystemException {
throw new FileSystemException("");
}
}
class Deri extends Base {
/* provide foo definition here */
}
Which of the following overriding definitions of the foo()
method in the Deri
class are compatible with the base class foo()
method definition? Choose ALL the foo()
method definitions that could compile without errors when put in the place of the comment: /* provide foo definition here */ A.
public void foo() throws IOException {
super.foo();
}
B.
public void foo() throws AccessDeniedException {
throw new AccessDeniedException("");
}
C.
public void foo() throws FileSystemException, RuntimeException {
throw new NullPointerException();
}
D.
public void foo() throws Exception {
throw new NullPointerException();
}
Consider the following program: class ChainedException {
public static void foo() {
try {
throw new ArrayIndexOutOfBoundsException();
} catch(ArrayIndexOutOfBoundsException oob) {
RuntimeException re = new RuntimeException(oob);
re.initCause(oob);
throw re;
}
}
public static void main(String []args) {
try {
foo();
} catch(Exception re) {
System.out.println(re.getClass());
}
}
}
When executed, this program prints which of the following? class java.lang.RuntimeException
class java.lang.IllegalStateException
class java.lang.Exception
class java.lang.ArrayIndexOutOfBoundsException
Consider the following program: class ExceptionTest {
public static void foo() {
try {
throw new ArrayIndexOutOfBoundsException();
} catch(ArrayIndexOutOfBoundsException oob) {
throw new Exception(oob);
}
}
public static void main(String []args) {
try {
foo();
} catch(Exception re) {
System.out.println(re.getCause());
}
}
}
Which one of the following options correctly describes the behavior of this program? java.lang.Exception
java.lang.ArrayIndexOutOfBoundsException
class java.lang.IllegalStateException
This program fails with compiler error(s) Consider the following program: import java.io.FileNotFoundException;
import java.sql.SQLException;
class MultiCatch {
public static void fooThrower() throws FileNotFoundException {
throw new FileNotFoundException();
}
public static void barThrower() throws SQLException {
throw new SQLException();
}
public static void main(String []args) {
try {
fooThrower();
barThrower();
} catch(FileNotFoundException || SQLException multie) {
System.out.println(multie);
}
}
}
Which one of the following options correctly describes the behavior of this program? This program prints the following: java.io.FileNotFoundException
This program prints the following: java.sql.SQLException
This program prints the following: java.io.FileNotFoundException || java.sql.SQLException
This program fails with compiler error(s) Consider the following class hierarchy from the package javax.security.auth.login
and answer the questions.
Which of the following handlers that makes use of multi-catch exception handler feature will compile without errors? catch (AccountException | LoginException exception)
catch (AccountException | AccountExpiredException exception)
catch (AccountExpiredException | AccountNotFoundException exception)
catch (AccountExpiredException exception1 | AccountNotFoundException exception2)
Consider the following code segment, which makes use of this exception hierarchy: try {
LoginException le = new AccountNotFoundException();
throw (Exception) le;
}
catch (AccountNotFoundException anfe) {
System.out.println("In the handler of AccountNotFoundException");
}
catch (AccountException ae) {
System.out.println("In the handler of AccountException");
}
catch (LoginException le) {
System.out.println("In the handler of LoginException");
}
catch (Exception e) {
System.out.println("In the handler of Exception");
}
When executed, which of the following statements will this code segment print? In the handler of AccountNotFoundException
In the handler of AccountException
In the handler of LoginException
In the handler of Exception
Consider the following program: import java.sql.SQLException;
class CustomSQLException extends SQLException {}
class BaseClass {
void foo() throws SQLException {
throw new SQLException();
}
}
class DeriClass extends BaseClass {
public void foo() throws CustomSQLException { // LINE A
throw new CustomSQLException();
}
}
class EHTest {
public static void main(String []args) {
try {
BaseClass base = new DeriClass();
base.foo();
} catch(Exception e) {
System.out.println(e);
}
}
}
Which one of the following options correctly describes the behavior of this program? The program prints the following: SQLException
The program prints the following: CustomSQLException
The program prints the following: Exception
When compiled, the program will result in a compiler error in line marked with comment Line A due to incompatible throws clause in the overridden foo
method Consider the following program: class EHBehavior {
public static void main(String []args) {
try {
int i = 10/0; // LINE A
System.out.print("after throw -> ");
} catch(ArithmeticException ae) {
System.out.print("in catch -> ");
return;
} finally {
System.out.print("in finally -> ");
}
System.out.print("after everything");
}
}
Which one of the following options best describes the behavior of this program? The program prints the following: in catch -> in finally -> after everything The program prints the following: after throw -> in catch -> in finally -> after everything The program prints the following: in catch -> after everything The program prints the following: in catch -> in finally -> When compiled, the program results in a compiler error in line marked with comment in LINE A for divide-by-zero Consider the following program: import java.util.Scanner;
class AutoCloseableTest {
public static void main(String []args) {
try (Scanner consoleScanner = new Scanner(System.in)) {
consoleScanner.close(); // CLOSE
consoleScanner.close();
}
}
}
Which one of the following statements is correct? This program terminates normally without throwing any exceptions This program throws an IllegalStateException
This program throws an IOException
This program throws an AlreadyClosedException
This program results in a compiler error in the line marked with the comment CLOSE
Consider the following program: class AssertionFailure {
public static void main(String []args) {
try {
assert false;
} catch(RuntimeException re) {
System.out.println("RuntimeException");
} catch(Exception e) {
System.out.println("Exception");
} catch(Error e) { // LINE A
System.out.println("Error" + e);
} catch(Throwable t) {
System.out.println("Throwable");
}
}
}
This program is invoked from the command line as follows: java AssertionFailure
Choose one of the following options describes the behavior of this program: Compiler error at line marked with comment LINE A Prints "RuntimeException" in console Prints "Exception" Prints "Error" Prints "Throwable" Does not print any output on console Consider the following program: import java.io.*;
class ExceptionTest {
public static void thrower() throws Exception {
try {
throw new IOException();
} finally {
throw new FileNotFoundException();
}
}
public static void main(String []args) {
try {
thrower();
} catch(Throwable throwable) {
System.out.println(throwable);
}
}
}
When executed, this program prints which one of the following? java.io.IOException java.io.FileNotFoundException java.lang.Exception java.lang.Throwable
答案:
Options B and C In option A and D, the throws clause declares to throw exceptions IOException
and Exception
respectively, which are more general than the FileSystemException
, so they are not compatible with the base method definition. In option B, the foo()
method declares to throw AccessDeniedException
, which is more specific than FileSystemException
, so it is compatible with the base definition of the foo()
method. In option C, the throws clause declares to throw FileSystemException
, which is the same as in the base definition of the foo()
method. Additionally it declares to throw RuntimeException
, which is not a checked exception, so the definition of the foo()
method is compatible with the base definition of the foo()
method. B. class java.lang.IllegalStateException
In the expression new RuntimeException(oob);
, the exception object oob
is already chained to the RuntimeException
object. The method initCause()
cannot be called on an exception object that already has an exception object chained during the constructor call. Hence, the call re.initCause(oob);
results in initCause()
throwing an IllegalStateException
. D. This program fails with compiler error(s) The foo()
method catches ArrayIndexOutOfBoundsException
and chains it to an Exception
object. However, since Exception
is a checked exception, it must be declared in the throws clause of foo()
. Hence this program results in this compiler error: ExceptionTest.java:6: error: unreported exception Exception; must be caught or declared to be thrown
throw new Exception(oob);
^
1 error
D. This program fails with compiler error(s) For multi-catch blocks, the single pipe (|) symbol needs to be used and not double pipe (||), as provided in this program. Hence this program will fail with compiler error(s). C. catch (AccountExpiredException
| AccountNotFoundException
exception) For A and B, the base type handler is provided with the derived type handler, hence the multi-catch is incorrect. For D, the exception name exception1 is redundant and will result in a syntax error. C is the correct option and this will compile fine without errors. A. In the handler of AccountNotFoundException
In this code, the created type of the exception is AccountNotFoundException
. Though the exception object is stored in the variable of type LoginException
and then type-casted to Exception
, the dynamic type of the exception remains the same, which is AccountNotFoundException
. When looking for a catch handler, the Java runtime looks for the exact handler based on the dynamic type of the object. Since it is available immediately as the first handler, this exactly matching catch handler got executed. B. The program prints the following: CustomSQLException
The exception thrown is CustomSQLException
object from the overridden foo
method in DeriClass
. Note that SQLException
is a checked exception since it extends the Exception class. Inside the BaseClass
, the foo
method lists SQLException
in its throws clause. In the DeriClass
, the overridden foo
method lists CustomSQLException
in its throws clause: it is acceptable to have a more restrictive exception throws clause in a derived class. Hence, the given program compiles successfully and prints CustomSQLException
. D. The program prints the following: in catch -> in finally -> The statement println("after throw -> ");
will never be executed since the line marked with the comment LINE A throws an exception. The catch handles ArithmeticException
, so println("in catch -> ");
will be executed. Following that, there is a return statement, so the function returns. But before the function returns, the finally statement should be called, hence the statement println("in finally -> ");
will get executed. So, the statement println("after everything");
will never get executed. A. This program terminates normally without throwing any exceptions The try-with-resources statement internally expands to call the close()
method in the finally block. If the resource is explicitly closed in the try block, then calling close()
again does not have any effect. From the description of the close()
method in the AutoCloseable
interface: “Closes this stream and releases any system resources associated with it. If the stream is already closed, then invoking this method has no effect.” F. Does not print any output on the console By default, assertions are disabled. If -ea
(or the -enableassertions
option to enable assertions), then the program would have printed "Error
" since the exception thrown in the case of assertion failure is java.lang.AssertionError
, which is derived from the Error
class. B. java.io.FileNotFoundException
If both the try block and finally block throw exceptions, the exception thrown from the try block will be ignored. Hence, the method thrower()
throws a FileNotFoundException
. The dynamic type of the variable throwable is FileNotFoundException
, so the program prints that type name.
八、使用 JavaSE8 日期/时间 API
认证目标 |
---|
使用 LocalDate、LocalTime、LocalDateTime、Instant、Period 和 Duration 创建和管理基于日期和基于时间的事件,包括将日期和时间组合到单个对象中 |
跨时区处理日期和时间,并管理夏令时带来的更改,包括日期和时间值的格式 |
使用 Instant、Period、Duration 和 TemporalUnit 定义、创建和管理基于日期和基于时间的事件 |
新的 Java 日期和时间 API 在java.time
包中提供。Java 8 中的这个新 API 取代了支持日期和时间相关功能的旧类,例如作为java.util
包的一部分提供的Date
、Calendar
和TimeZone
类。
Java 8 已经有了早期 Java 的Date
和Calendar
等类,为什么还要引入新的日期和时间 API?主要原因是不方便的 API 设计。例如,Date
类既有日期又有时间;如果只需要时间信息而不需要与日期相关的信息,则必须将与日期相关的值设置为零。这些课程的某些方面也不直观。例如,在Date
构造函数中,日期值的范围是 1 到 31,而月份值的范围是 0 到 11(不是 1 到 12)!此外,java.util.Date
和SimpleDateFormatter
还有许多与并发相关的问题,因为它们不是线程安全的。
Java 8 在新引入的java.time
包中为日期和时间相关的功能提供了很好的支持。这个包中的大多数类都是不可变的和线程安全的。本章说明了如何使用这个包中的重要类和接口,包括LocalDate
、LocalTime
、LocalDateTime
、Instant
、Period
、Duration
和TemporalUnit
。您还将学习如何使用时区和夏令时,以及如何设置日期和时间值的格式。
API 包含了流畅接口的概念:它的设计方式使得代码可读性更强,也更容易使用。因此,这个包中的类有许多静态方法(其中许多是工厂方法)。此外,这些类中的方法遵循一个通用的命名约定(例如,它们使用前缀plus
和minus
来加减日期或时间值)。
理解 java.time 中的重要类
认证目标 |
---|
使用 LocalDate、LocalTime、LocalDateTime、Instant、Period 和 Duration 创建和管理基于日期和基于时间的事件,包括将日期和时间组合到单个对象中 |
使用 Instant、Period、Duration 和 TemporalUnit 定义、创建和管理基于日期和基于时间的事件 |
java.time
包由四个子包组成:
java.time.temporal
—访问日期/时间字段和单位java.time.format
—格式化日期/时间对象的输入和输出java.time.zone
—处理时区java.time.chrono
—支持日本和泰国日历等日历系统
本章仅关注考试目标所涵盖的日期/时间主题。让我们从学习使用LocalDate
、LocalTime
、LocalDateTime
、Instant
、Period
和Duration
类开始。
使用 LocalDate 类
java.time.
LocalDate
表示没有时间或时区的日期。LocalDate
在 ISO-8601 日历系统中以年-月-日格式(YYYY-MM-DD)表示:例如,2015-10-26。
Java 8 日期和时间 API 使用 ISO 8601 作为默认的日历格式。在这种国际公认的格式中,日期和时间值从最大到最小的时间单位排序:年、月/周、日、小时、分钟、秒和毫秒/纳秒。
这里有一个使用LocalDate
的例子:
LocalDate today = LocalDate.now();
System.out.println("Today's date is: " + today);
当我们运行该代码时,它打印出以下内容:
Today's date is: 2015-10-26
LocalDate.now()
方法基于默认时区,使用系统时钟获取当前日期。您可以通过显式指定日、月和年组件来获得一个LocalDate
对象:
LocalDate newYear2016 = LocalDate.of(2016, 1, 1);
System.out.println("New year 2016: " + newYear2016);
该代码打印以下内容:
New year 2016: 2016-01-01
这个代码怎么样?
LocalDate valentinesDay = LocalDate.of(2016, 14, 2);
System.out.println("Valentine's day is on: " + valentinesDay);
它抛出一个异常:
Exception in thread "main" java.time.DateTimeException: Invalid value for MonthOfYear
(valid values 1 - 12): 14
在这种情况下,month
和dayOfMonth
参数值互换。LocalDate
的of()
方法声明如下:
LocalDate of(int year, int month, int dayOfMonth)
为了避免犯这个错误,可以使用重载版本LocalDate.of(int year, Month month, int day)
。第二个参数java.time.Month
是一个枚举,表示一年中的 12 个月。如果互换日期和月份参数,就会出现编译器错误。下面是使用此枚举的改进版本:
LocalDate valentinesDay = LocalDate.of(2016, Month.FEBRUARY, 14);
System.out.println("Valentine's day is on: " + valentinesDay);
这段代码打印出来
Valentine's day is on: 2016-02-14
LocalDate
类有一些方法,你可以用它们在当前的LocalDate
对象上增加或减少日、周、月或年。例如,假设你的签证在 180 天后到期。以下是显示到期日期的代码段(假设今天的日期是 2015-10-26 )
:
long visaValidityDays = 180L;
LocalDate currDate = LocalDate.now();
System.out.println("My Visa expires on: " + currDate.plusDays(visaValidityDays));
这段代码显示了以下内容:
My Visa expires on: 2016-04-23
除了plusDays()
方法,LocalDate
还提供了plusWeeks()
、plusMonths()
和plusYears()
方法,以及用于减法的方法:minusDays()
、minusWeeks()
、minusMonths()
和minusYears()
。表 8-1 列出了你需要知道的LocalDate
类中的更多方法(这个表提到了像ZoneId
这样的类——它们将在本章后面讨论)。
表 8-1。
Important Methods in the LocalDate
Class
方法 | 简短描述 | 示例代码 |
---|---|---|
LocalDate now(Clock clock) LocalDate now(ZoneId zone) |
使用传递的clock 或zone 参数返回带有当前日期的LocalDate 对象 |
// assume today's date is 26 Oct 2015``LocalDate.now(Clock.systemDefaultZone());``// returns current date as 2015-10-26``LocalDate.now(ZoneId.of("Asia/Kolkata"));``// returns current date as 2015-10-26``LocalDate.now(ZoneId.of("Asia/Tokyo")); |
LocalDate ofYearDay(int year, int dayOfYear) |
从作为参数传递的year 和dayOfYear 中返回LocalDate |
LocalDate.ofYearDay(2016,100); // returns date as 2016-04-09 |
LocalDate parse(CharSequence dateString) |
从作为参数传递的dateString 中返回LocalDate |
LocalDate.parse("2015-10-26");``// returns a LocalDate corresponding``// to the passed string argument; hence it |
LocalDate ofEpochDay(Long epochDay) |
通过将天数添加到纪元开始日(纪元开始于 1970 年)来返回LocalDate |
LocalDate.ofEpochDay(10); // returns 1970-01-11; |
使用 LocalTime 类
除了LocalTime
表示没有日期或时区的时间之外,java.time.LocalTime
类与LocalDate
类似。时间采用 ISO-8601 日历系统格式:HH:MM:SS.nanosecond
。LocalTime
和LocalDate
都使用系统时钟和默认时区。
下面是一个使用LocalTime
的例子:
LocalTime currTime = LocalTime.now();
System.out.println("Current time is: " + currTime);
当我们执行它时,它打印了以下内容:
Current time is: 12:23:05.072
如上所述,LocalTime
使用系统时钟及其默认时区。要基于特定的时间值创建不同的时间对象,可以使用LocalTime
类的重载of()
方法:
System.out.println(LocalTime.of(18,30));
// prints: 18:30
LocalTime
提供了许多有用的方法,可以用来加减小时、分钟、秒和纳秒。例如,假设从现在起 6.5 小时后您有一个会议,您想要找到确切的会议时间。这里有一段代码:
long hours = 6;
long minutes = 30;
LocalTime currTime = LocalTime.now();
System.out.println("Current time is: " + currTime);
System.out.println("My meeting is at: " + currTime.plusHours(hours).plusMinutes(minutes));
这段代码显示了以下内容:
Current time is: 12:29:13.624
My meeting is at: 18:59:13.624
除了plusHours()
,LocalTime
还支持plusMinutes()
,plusSeconds()
,plusNanos()
方法;同样,对于减法,它支持minusHours()
、minusMinutes()
、minusNanos()
和minusSeconds()
。表 8-2 列出了LocalTime
类中的一些重要方法。
表 8-2。
Important Methods in the LocalTime
Class
方法 | 简短描述 | 示例代码 |
---|---|---|
LocalTime now(Clock clock) LocalTime now(ZoneId zone) |
使用传递的clock 或zone 参数返回带有当前时间的LocalTime 对象 |
LocalTime.now(Clock.systemDefaultZone())``// returns current time as 18:30:35.744``LocalDate.now(ZoneId.of("Asia/Tokyo"); |
LocalTime ofSecondOfDay(long daySeconds) |
从作为参数传递的daySeconds 中返回LocalTime (注意,一天 24 小时有 86,400 秒) |
LocalTime.ofSecondOfDay(66620);``// returns 18:30:20 because |
LocalTime parse(CharSequence timeString) |
从作为参数传递的dateString 中返回LocalTime |
LocalTime.parse("18:30:05");``// returns a LocalTime object``// corresponding to the given String |
使用 LocalDateTime 类
类别java.time.
LocalDateTime
表示没有时区的日期和时间。您可以将LocalDateTime
视为LocalTime
和LocalDate
类的逻辑组合。日期和时间格式使用 ISO-8601 日历系统:YYYY-MM-DD HH:MM:SS.nanosecond
。
下面是一个打印今天的日期和当前时间的简单示例:
LocalDateTime currDateTime = LocalDateTime.now();
System.out.println("Today's date and current time is: " + currDateTime);
当我们运行这段代码时,它打印出以下内容:
Today's date and current time is: 2015-10-29T21:04:36.376
在这个输出中,注意字符 T 代表时间,它分隔了日期和时间部分。使用系统时钟及其默认时区获取当前日期和时间。
java.time
包中的很多类,包括LocalDate
、LocalTime
、LocalDateTime
,都支持isAfter()
和isBefore()
方法进行比较:
LocalDateTime christmas = LocalDateTime.of(2015, 12, 25, 0, 0);
LocalDateTime newYear = LocalDateTime.of(2016, 1, 1, 0, 0);
System.out.println("New Year 2016 comes after Christmas 2015? "+newYear.isAfter(christmas));
该代码打印以下内容:
New Year 2016 comes after Christmas 2015? true
您可以分别使用toLocalDate()
和toLocalTime()
方法从给定的LocalDateTime
对象中获取LocalDate
和LocalTime
对象:
LocalDateTime dateTime = LocalDateTime.now();
System.out.println("Today's date and current time: " + dateTime);
System.out.println("The date component is: " + dateTime.toLocalDate());
System.out.println("The time component is: " + dateTime.toLocalTime());
当我们执行这段代码时,它打印出来
Today's date and current time: 2015-11-04T13:19:10.497
The date component is: 2015-11-04
The time component is: 13:19:10.497
与表 8-1 和 8-2 中列出的方法类似,LocalDateTime
有now()
、of()
、parse()
等方法。同样,类似于LocalDate
和LocalTime
,这个类也提供了加减年、月、日、小时、分钟、秒和纳秒的方法。为避免重复,这里不再列出这些方法。
使用即时类
假设您想要跟踪一个 Java 应用的执行,或者将应用事件存储在一个文件中。出于这些目的,您需要获得时间戳值,您可以使用java.time.Instant
类来实现。瞬时值开始于 1970 年 1 月 1 日 00:00:00 小时(称为 Unix 纪元)。
Instant
类在内部使用一个long
变量,该变量保存从 Unix 纪元开始以来的秒数:1970-01-01T00:00:00Z
(出现在这个纪元之前的值被视为负值)。此外,Instant
使用一个整数变量来存储每秒经过的纳秒数。清单 8-1 中的程序使用了Instant
类。
Listing 8-1. UsingInstant.java
import java.time.Instant;
public class UsingInstant {
public static void main(String args[]){
// prints the current timestamp with UTC as time zone
Instant currTimeStamp = Instant.now();
System.out.println("Instant timestamp is: "+ currTimeStamp);
// prints the number of seconds as Unix timestamp from epoch time
System.out.println("Number of seconds elapsed: " + currTimeStamp.getEpochSecond());
// prints the Unix timestamp in milliseconds
System.out.println("Number of milliseconds elapsed: " + currTimeStamp.toEpochMilli());
}
}
执行时,它会打印以下内容:
Instant timestamp is: 2015-11-02T03:16:04.502Z
Number of seconds elapsed: 1446434164
Number of milliseconds elapsed: 1446434164502
LocalDateTime
和Instant
有什么区别?这里有一个例子可以说明:
LocalDateTime localDateTime = LocalDateTime.now();
Instant instant = Instant.now();
System.out.println("LocalDateTime is: " + localDateTime + " \nInstant is: " + instant);
当我们执行此操作时,它打印了以下内容:
LocalDateTime is: 2015-11-02T
17:21:11.402
Instant is: 2015-11-02T
11:51:11.404Z
如你所见,LocalDateTime
打印的时间值与Instant
的结果不同。为什么呢?因为我们生活在亚洲/加尔各答时区,与格林威治时间相差+05:30 小时。LocalDateTime
使用默认时区,但Instant
没有。
使用 Period 类
java.time.Period
类用于根据年、月和日来测量时间量。假设你买了一些很贵的药,想在过期前用完。你可以通过下面的方法知道它什么时候到期:
LocalDate manufacturingDate = LocalDate.of(2016, Month.JANUARY, 1);
LocalDate expiryDate = LocalDate.of(2018, Month.JULY, 18);
Period expiry = Period.between(manufacturingDate, expiryDate);
System.out.printf("Medicine will expire in: %d years, %d months, and %d days (%s)\n",
expiry.getYears(), expiry.getMonths(), expiry.getDays(), expiry);
这段代码显示了以下内容:
Medicine will expire in: 2 years, 6 months, and 17 days (P2Y6M17D)
这个例子使用了Period.between()
方法,该方法将两个LocalDate
值作为参数,并返回一个Period
。这个程序使用方法getYears()
、getMonths()
和getDays()
(这三个方法返回一个int
值),它们分别返回给定期间的年数、月数和天数。Period
的toString()
方法打印值P2Y6M17D
。在该字符串中,字符P
、Y
、M
和D
分别代表周期、年、月和日。
从Period
开始,您可以使用plusYears()
、plusMonths()
、plusDays()
、minusYears()
、minusMonths()
和minusDays()
的方法增加或减少年、月和日。表 8-3 列出了这个类中的其他重要方法。
表 8-3。
Important Methods in the Period
Class
方法 | 简短描述 | 示例代码 |
---|---|---|
Period of(int years, int months, int days) |
基于给定的参数返回一个Period 对象 |
LocalDate christmas = LocalDate.of(2015, 12, 25);``System.out.println(``Period.between(LocalDate.now(), christmas)); |
Period ofWeeks(int unit)``Period ofDays(int unit)``Period ofMonths(int unit) |
基于参数中的单位返回一个Period 对象 |
Period.ofWeeks(2) // returns P14D Period.ofDays(15) // returns P15D Period.ofMonths(6) // returns P6M Period.ofYears(4) // returns P4Y |
Period parse(CharSequence string) |
从作为参数传递的string 中返回一个Period |
Period.parse("P4Y6M15D"); // returns P4Y6M15D |
Java 8 日期和时间 API 区分了人类和计算机使用日期和时间相关信息的方式。例如,
Instant
类表示一个 Unix 时间戳,并在内部使用long
和int
变量。Instant
人类不太容易读懂或使用这些值,因为该类不支持与日、月、小时等相关的方法(相比之下,Period
类支持这些方法)。
使用持续时间类
我们之前讨论过Period
类——它用年、月和日来表示时间。Duration
相当于Period
的时间。Duration
类用小时、分钟、秒等表示时间。适用于测量机器时间或使用Instance
物体时。类似于Instance
类,Duration
类将秒部分存储为long
值,并将纳秒存储为int
值。
说你想在今晚午夜祝你最好的朋友贝基生日快乐。你可以通过下面的方法来确定需要多少小时:
LocalDateTime comingMidnight =
LocalDateTime.of(LocalDate.now().plusDays(1), LocalTime.MIDNIGHT);
LocalDateTime now = LocalDateTime.now();
Duration between = Duration.between(now, comingMidnight);
System.out.println(between);
该代码打印以下内容:
PT7H13M42.003S
这个例子使用了LocalDateTime
类中of()
方法的重载版本:LocalDateTime of(LocalDate, LocalTime)
。LocalDate.now()
调用返回当前日期,但是您需要给这个值加上一天,这样您就可以使用LocalTime.MIDNIGHT
来表示即将到来的午夜。Duration
中的between()
方法接受两个时间值——在本例中是两个LocalDateTime
对象。当我们执行这个程序时,时间是 16:46:17;从那时到午夜是 7 小时 13 分 42 秒。这由toString()
输出、Period
: PT7H13M42.003S
指示。前缀PT
表示周期时间,H
表示小时,M
表示分钟,S
表示秒。
表 8-4 列出了Duration
类的一些重要方法。TemporalUnit
和ChronoUnit
将在本章后面讨论。
图 8-1。
Summary of the Instant
, Period
, and Duration
classes
表 8-4。
Important Methods in the Duration
Class
方法 | 简短描述 | 示例代码 |
---|---|---|
Duration of(long number, TemporalUnit unit)``Duration ofDays(long unit)``Duration ofHours(long unit)``Duration ofMinutes(long unit)``Duration ofSeconds(long unit)``Duration ofMillis(long unit) |
以指定格式返回给定数字的Duration 对象,根据参数中给定的单位返回Duration |
Duration.of(3600, ChronoUnit.MINUTES) // returns "PT60H"``Duration.ofDays(4)``// returns "PT96H"``Duration.ofHours(2)``// returns "PT2H"``Duration.ofMinutes(15)``// returns "PT15M"``Duration.ofSeconds(30)``//returns "PT30S"``Duration.ofMillis(120)``// returns "PT0.12S"``Duration.ofNanos(120)``// returns "PT0.00000012S" |
Duration parse(CharSequence string) |
从作为参数传递的string 中返回一个Period |
Duration.parse("P2DT10H30M")``// returns a Duration object |
使用 TemporalUnit 接口
TemporalUnit
接口是java.time.temporal
包的一部分。它表示日期或时间单位,如秒、小时、天、月、年等。枚举java.time.temporal.ChronoUnit
实现了这个接口。与其使用常数值,不如使用它们的等价枚举值。为什么?因为在ChronoUnit
中使用枚举值会产生更可读的代码;此外,你不太可能犯错误。
清单 8-2 打印枚举值,无论它们是基于日期还是基于时间,以及持续时间。
Listing 8-2. ChronoUnitValues.java
import java.time.temporal.ChronoUnit;
public class ChronoUnitValues {
public static void main(String []args) {
System.out.println("ChronoUnit DateBased TimeBased Duration");
System.out.println("---------------------------------------");
for(ChronoUnit unit : ChronoUnit.values()) {
System.out.printf("%10s \t %b \t\t %b \t\t %s %n",
unit, unit.isDateBased(), unit.isTimeBased(), unit.getDuration());
}
}
}
结果如下:
ChronoUnit DateBased TimeBased Duration
--------------------------------------- --------------------
Nanos false true PT0.000000001S
Micros false true PT0.000001S
Millis false true PT0.001S
Seconds false true PT1S
Minutes false true PT1M
Hours false true PT1H
HalfDays false true PT12H
Days true false PT24H
Weeks true false PT168H
Months true false PT730H29M6S
Years true false PT8765H49M12S
Decades true false PT87658H12M
Centuries true false PT876582H
Millennia true false PT8765820H
Eras true false PT8765820000000H
Forever false false PT2562047788015215H30M7.999999999S
java.time
包中的众多方法都以TemporalUnit
为自变量。例如,考虑一下Duration
类中的of()
方法:
Duration of(long amount, TemporalUnit unit)
因为ChronoUnit
枚举实现了TemporalUnit
接口,所以您可以传递一个ChronoUnit
枚举值作为该构造函数的第二个参数:
System.out.println(Duration.of(1, ChronoUnit.MINUTES).getSeconds());
// prints: 60
System.out.println(Duration.of(1, ChronoUnit.HOURS).getSeconds());
// prints:3600
System.out.println(Duration.of(1, ChronoUnit.DAYS).getSeconds());
// prints: 86400
从这个例子中可以看出,ChronoUnit
帮助您处理时间单位值,如秒、分、小时,以及日期值,如日、月和年。
处理时区和夏令时
认证目标 |
---|
跨时区处理日期和时间,并管理夏令时带来的更改,包括日期和时间值的格式 |
前一节讨论了java.time
包中的一些重要类。本节讨论如何跨时区处理日期和时间、处理夏令时以及设置日期和时间值的格式。
使用时区相关的类
为了跨时区处理日期和时间,您需要了解三个与时区相关的重要类:ZoneId
、ZoneOffset
和ZonedDateTime
。现在我们来讨论一下。
使用 ZoneId 类
在java.time
包中,java.time.ZoneId
类代表时区。时区通常使用格林威治标准时间(GMT,也称为 UTC/格林威治时间)的偏移量来标识。
例如,我们生活在印度,而印度唯一的时区是亚洲/加尔各答(使用这种地区/城市格式给出时区)。此代码打印时区:
System.out.println("My zone id is: " + ZoneId.systemDefault());
对于我们的时区,它打印如下:
My zone id is: Asia/Kolkata
您可以通过调用ZoneId
中的静态方法getAvailableZoneIds()
获得时区列表,该方法返回一个Set<String>
:
Set<String> zones = ZoneId.getAvailableZoneIds();
System.out.println("Number of available time zones is: " + zones.size());
zones.forEach(System.out::println);
结果如下:
Number of available time zones is: 589
Asia/Aden
America/Cuiaba
// rest of the output elided...
您可以将这些时区标识符中的任何一个传递给of()
方法来创建相应的ZoneId
对象,如
ZoneId AsiaKolkataZoneId = ZoneId.of("Asia/Kolkata");
使用 ZoneOffset 类
ZoneId
标识时区,如Asia/Kolkata
。另一个类ZoneOffset
,代表相对于 UTC/格林威治的时区偏移量。例如,时区 ID“亚洲/加尔各答”相对于 UTC/格林威治的时区偏移量为+05:30(加 5 小时 30 分钟)。ZoneOffset
类扩展了ZoneId
类。我们将在下一节讨论一个使用ZoneOffset
的例子。
使用 ZonedDateTime 类
在 Java 8 中,如果只想处理日期、时间或时区,可以分别使用LocalDate
、LocalTime
或ZoneId
。如果您希望日期、时间和时区这三者都包含在内,该怎么办呢?为此,您可以使用ZonedDateTime
类:
LocalDate currentDate = LocalDate.now();
LocalTime currentTime = LocalTime.now();
ZoneId myZone = ZoneId.systemDefault();
ZonedDateTime zonedDateTime = ZonedDateTime.of(currentDate, currentTime, myZone);
System.out.println(zonedDateTime);
结果如下:
2015-11-05T11:38:40.647+05:30[Asia/Kolkata]
这段代码使用了重载的静态方法ZonedDateTime of(LocalDate, LocalTime, ZoneID)
。
给定一个LocalDateTime
,你可以用一个ZoneId
得到一个ZonedDateTime
对象:
LocalDateTime dateTime = LocalDateTime.now();
ZoneId myZone = ZoneId.systemDefault();
ZonedDateTime zonedDateTime = dateTime.atZone(myZone);
为了说明这些不同时区相关类之间的转换,下面的代码段创建了一个ZoneId
对象,将时区信息添加到一个LocalDateTime
对象中以获得一个ZonedDateTime
对象,最后从ZonedDateTime
中获得时区偏移量:
ZoneId myZone = ZoneId.of("Asia/Kolkata");
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = dateTime.atZone(myZone);
ZoneOffset zoneOffset = zonedDateTime.getOffset();
System.out.println(zoneOffset);
它打印以下内容:
+05:30
假设你在新加坡,日期是 2016 年 1 月 1 日,时间是早上 6:00,在和住在奥克兰(新西兰)的朋友通话之前,你想了解一下新加坡和奥克兰的时差。清单 8-3 展示了一个使用ZoneId
、ZonedDateTime
和Duration
类的程序,以说明如何一起使用这些类。
Listing 8-3. TimeDifference.java
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.Duration;
public class TimeDifference {
public static void main(String[] args) {
ZoneId singaporeZone = ZoneId.of("Asia/Singapore");
ZonedDateTime dateTimeInSingapore = ZonedDateTime.of(
LocalDateTime.of(2016, Month.JANUARY, 1, 6, 0), singaporeZone);
ZoneId aucklandZone = ZoneId.of("Pacific/Auckland");
ZonedDateTime sameDateTimeInAuckland =
dateTimeInSingapore.withZoneSameInstant(aucklandZone);
Duration timeDifference = Duration.between(
dateTimeInSingapore.toLocalTime(),
sameDateTimeInAuckland.toLocalTime());
System.out.printf("Time difference between %s and %s zones is %d hours",
singaporeZone, aucklandZone, timeDifference.toHours());
}
}
结果如下:
Time difference between Asia/Singapore and Pacific/Auckland zones is 5 hours
这个程序创建了两个ZoneId
:一个用于新加坡,另一个用于奥克兰。在用给定的日期和时间为新加坡时区创建了一个ZonedDateTime
对象之后,通过调用ZonedDateTime
类的withZoneSameInstant()
方法,您得到了奥克兰的等效的ZonedDateTime
对象。要找到以小时为单位的时差,可以使用Duration
的Duration.between()
方法和toHours()
方法。
应对夏令时
因为季节变化,一年中的日照量不会保持不变。例如,夏天有更多的日光。使用夏令时(DST)时,时钟会提前或推迟一小时,以充分利用日光。俗话说,“春去秋来”——春天开始时,时钟通常会提前一个小时,秋天开始时,时钟会推迟一个小时:
ZoneId kolkataZone = ZoneId.of("Asia/Kolkata");
Duration kolkataDST = kolkataZone.getRules().getDaylightSavings(Instant.now());
System.out.printf("Kolkata zone DST is: %d hours %n", kolkataDST.toHours());
ZoneId aucklandZone = ZoneId.of("Pacific/Auckland");
Duration aucklandDST = aucklandZone.getRules().getDaylightSavings(Instant.now());
System.out.printf("Auckland zone DST is: %d hours", aucklandDST.toHours());
以下是结果(2015 年 11 月 5 日执行时):
Kolkata zone DST is: 0 hours
Auckland zone DST is: 1 hours
调用zoneId.getRules().getDaylightSavings(Instant.now());
根据当时 DST 是否有效返回一个Duration
对象。如果Duration.isZero()
为假,DST 在该区域有效;否则,就不是。在本例中,加尔各答时区不采用夏令时,但奥克兰时区采用+1 小时的夏令时。
格式化日期和时间
当用日期和时间编程时,你经常需要用不同的格式打印它们。此外,您可能需要阅读不同格式的日期/时间信息。要读取或打印各种格式的日期和时间值,可以使用java.time.format
包中的DateTimeFormatter
类。
DateTimeFormatter
类提供了许多用于格式化日期和时间值的预定义常量。以下是一些这样的预定义格式化程序的列表(带有示例输出值):
ISO_DATE
(2015-11-05)ISO_TIME
(11:25:47.624)RFC_1123_DATE_TIME
(从而,2015 年 11 月 5 日上午 11:27:22 +0530)ISO_ZONED_DATE_TIME
(2015-11-05t 11:30:33.49+05:30[亚洲/加尔各答])
下面是一个简单的例子,它使用了类型为DateTimeFormatter
的预定义的ISO_TIME
:
LocalTime wakeupTime = LocalTime.of(6, 0, 0);
System.out.println("Wake up time: " + DateTimeFormatter.ISO_TIME.format(wakeupTime));
这印刷了以下内容:
Wake up time: 06:00:00
如果您想使用自定义格式而不是任何预定义的格式,该怎么办?为此,您可以使用DateTimeFormatter
类中的ofPattern()
方法:
DateTimeFormatter customFormat = DateTimeFormatter.ofPattern("dd MMM yyyy");
System.out.println(customFormat.format(LocalDate.of(2016, Month.JANUARY, 01)));
结果如下:
01 Jan 2016
使用字母对日期或时间的格式进行编码,以形成日期或时间模式字符串。通常这些字母在模式中重复出现。
在日期和时间的格式字符串中使用时,大写和小写字母可以有相似或不同的含义。在尝试使用这些字母之前,请仔细阅读这些模式的 Javadoc。比如在 dd-MM-yy 中,MM 指的是月;但是,在 dd-mm-yy 中,mm 指的是分钟!
前面的代码段给出了一个创建自定义日期格式的简单示例。类似的字母可用于创建自定义日期和时间模式字符串。以下是创建日期模式的重要字母及其含义的列表(带有示例):
G
(时代:BC,AD)y
(纪元年份:2015 年 15 月)Y
(以周为单位的年份:2015 年 15 月)M
(月:11 日、11 月、11 月)w
(一年中的第 13 周)W
(月中的星期:2)E
(一周中的日名:星期日、星期日)D
(一年中的第 256 天)d
(一月中的第几天:13)
清单 8-4 中的程序使用简单和复杂的模式字符串来创建定制的日期格式。
Listing 8-4. CustomDatePatterns.java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class CustomDatePatterns {
public static void main(String []args) {
// patterns from simple to complex ones
String [] dateTimeFormats = {
"dd-MM-yyyy", /* d is day (in month), M is month, y is year */
"d '('E')' MMM, YYYY", /*E is name of the day (in week), Y is year*/
"w'th week of' YYYY", /* w is the week of the year */
"EEEE, dd'th' MMMM, YYYY" /*E is day name in the week */
};
LocalDateTime now = LocalDateTime.now();
for(String dateTimeFormat : dateTimeFormats) {
System.out.printf("Pattern \"%s\" is %s %n", dateTimeFormat,
DateTimeFormatter.ofPattern(dateTimeFormat).format(now));
}
}
}
结果如下:
Pattern "dd-MM-yyyy" is 05-11-2015
Pattern "d '('E')' MMM, YYYY" is 5 (Thu) Nov, 2015
Pattern "w'th week of' YYYY" is 45th week of 2015
Pattern "EEEE, dd'th' MMMM, YYYY" is Thursday, 05th November, 2015
如您所见,重复的字母导致条目的形式更长。例如,当您使用E
(这是一周中的某一天的名称)时,结果是“Thu”,而使用EEEE
则打印完整形式的日期名称,即“星期四”。
另一个需要注意的重要事情是如何在给定的模式字符串中打印文本。为此,您使用单引号分隔的文本,由DateTimeFormatter
按原样打印。比如'('E')'
打印“(Wed)”。如果您给出了一个不正确的模式,或者忘记使用单引号将文本与模式字符串中的模式字母分开,那么您会因为传递了一个“非法模式”而得到一个DateTimeParseException
现在,让我们看一个创建定制时间模式字符串的类似例子。以下是定义自定义时间模式的重要字母列表:
a
(文本标记上午/下午标记)H
(小时:取值范围 0-23)k
(小时:取值范围 1–24)K
(上午/下午的小时:取值范围 0-11)h
(上午/下午的小时:取值范围 1-12)m
(分钟s
(第二次)S
(几分之一秒)z
(时区:通用时区格式)
有关更多字母及其描述,请参见DateTimeFormatter
类的 Javadoc。清单 8-5 展示了一个使用简单和复杂模式字符串创建定制时间格式的程序。
Listing 8-5. CustomTimePatterns.java
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
// Using examples, illustrates how to use "pattern strings" for creating custom time formats
class CustomTimePatterns {
public static void main(String []args) {
// patterns from simple to complex ones
String [] timeFormats = {
"h:mm", /* h is hour in am/pm (1-12), m is minute */
"hh 'o''clock'", /* '' is the escape sequence to print a single quote */
"H:mm a", /* H is hour in day (0-23), a is am/pm*/
"hh:mm:ss:SS", /* s is seconds, S is milliseconds */
"K:mm:ss a" /* K is hour in am/pm(0-11) */
};
LocalTime now = LocalTime.now();
for(String timeFormat : timeFormats) {
System.out.printf("Time in pattern \"%s\" is %s %n", timeFormat,
DateTimeFormatter.ofPattern(timeFormat).format(now));
}
}
}
结果如下:
Time in pattern "h:mm" is 12:27
Time in pattern "hh 'o''clock'" is 12 o'clock
Time in pattern "H:mm a" is 12:27 PM
Time in pattern "hh:mm:ss:SS" is 12:27:10:41
Time in pattern "K:mm:ss a" is 0:27:10 PM
注意基于所使用的模式字符串,输出是如何不同的。
航班旅行示例
让我们看一个例子,它使用了到目前为止所涉及的许多类。假设你需要赶 2016 年 1 月 1 日早上 6:00 从新加坡出发的航班,航班需要 10 个小时到达新西兰奥克兰。你能知道到达奥克兰的时间吗?清单 8-6 中的程序解决了这个问题。
Listing 8-6. FlightTravel.java
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FlightTravel {
public static void main(String[] args) {
DateTimeFormatter dateTimeFormatter =
DateTimeFormatter.ofPattern("dd MMM yyyy hh.mm a");
// Leaving on 1st Jan 2016, 6:00am from "Singapore"
ZonedDateTime departure = ZonedDateTime.of(
LocalDateTime.of(2016, Month.JANUARY, 1, 6, 0),
ZoneId.of("Asia/Singapore"));
System.out.println("Departure: " + dateTimeFormatter.format(departure));
// Arrival on the same day in 10 hours in "Auckland"
ZonedDateTime arrival =
departure.withZoneSameInstant(ZoneId.of("Pacific/Auckland"))
.plusHours(10);
System.out.println("Arrival: " + dateTimeFormatter.format(arrival));
}
}
结果如下:
Departure: 01 Jan 2016 06.00 AM
Arrival: 01 Jan 2016 09.00 PM
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
使用 LocalDate、LocalTime、LocalDateTime、Instant、Period 和 Duration 创建和管理基于日期和基于时间的事件,包括将日期和时间组合到单个对象中
- Java 8 日期和时间 API 使用 ISO 8601 作为默认的日历格式。
java.time.LocalDate
类表示没有时间或时区的日期;java.time.LocalTime
类表示没有日期和时区的时间;java.time.LocalDateTime
类表示没有时区的日期和时间。java.time.Instant
类表示一个 Unix 时间戳。java.time.Period
用于以年、月和日来度量时间。java.time.Duration
类用小时、分钟、秒和秒的分数来表示时间。
跨时区处理日期和时间,并管理夏令时带来的更改,包括日期和时间值的格式
ZoneId
标识一个时区;ZoneOffset
表示相对于 UTC/格林威治的时区偏移量。ZonedDateTime
提供对所有三个方面的支持:日期、时间和时区。- 在不同时区工作时,您必须考虑夏令时(DST)。
java.time.format.DateTimeFormatter
类支持读取或打印不同格式的日期和时间值。DateTimeFormatter
类提供了用于格式化日期和时间值的预定义常量(如ISO_DATE
和ISO_TIME
)。- 使用区分大小写的字母对日期或时间的格式进行编码,用
DateTimeFormatter
类形成日期或时间模式字符串。
使用 Instant、Period、Duration 和 TemporalUnit 定义、创建和管理基于日期和基于时间的事件
- 枚举
java.time.temporal.ChronoUnit
实现了java.time.temporal.TemporalUnit
接口。 TemporalUnit
和ChronoUnit
都处理时间单位值,如秒、分和小时,以及日期值,如日、月和年。
Question TimeChoose the correct option based on this code segment: LocalDate babyDOB = LocalDate.of(2015, Month.FEBRUARY, 20);
LocalDate now = LocalDate.of(2016, Month.APRIL, 10);
System.out.println(Period.between(now, babyDOB).getYears()); // PERIOD_CALC
The code segment results in a compiler error in the line marked with the comment PERIOD_CALC
The code segment throws a DateTimeException
The code segment prints: 1
The code segment prints: -1
Which one of the following classes is best suited for storing timestamp values of application events in a file? java.time.ZoneId
class java.time.ZoneOffset
class java.time.Instant
class java.time.Duration
class java.time.Period
class Given this code segment ZoneId zoneId = ZoneId.of("Asia/Singapore");
ZonedDateTime zonedDateTime =
ZonedDateTime.of(LocalDateTime.now(), zoneId);
System.out.println(zonedDateTime.getOffset());
assume that the time-offset value for the Asia/Singapore time zone from UTC/Greenwich is +08:00. Choose the correct option. This code segment results in throwing DateTimeException
This code segment results in throwing UnsupportedTemporalTypeException
The code segment prints: Asia/Singapore
The code segment prints: +08:00
This code segment prints: +08:00
[Asia/Singapore]
Choose the correct option based on this code segment: DateTimeFormatter dateFormat = DateTimeFormatter.ISO_DATE; // DEF
LocalDate dateOfBirth = LocalDate.of(2015, Month.FEBRUARY, 31);
System.out.println(dateFormat.format(dateOfBirth)); // USE
The program gives a compiler error in the line marked with the comment DEF
The program gives a compiler error in the line marked with the comment USE
The code segment prints: 2015-02-31
The code segment prints: 2015-02-03
This code segment throws java.time.DateTimeException
with the message "Invalid date 'FEBRUARY 31'"
Consider this code segment: DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("EEEE", Locale.US);
System.out.println(formatter.format(LocalDateTime.now()));
Which of the following outputs matches the string pattern "EEEE"
given in this code segment? F Friday Sept September
答案:
The code segment prints: -1
Here are the arguments to the between()
method in the Period
class: Period between(LocalDate startDateInclusive, LocalDate endDateExclusive)
The first argument is the start and the second argument is the end, and hence the call Period.between(now, babyDOB)
results in -1 (not +1). C. Instant
class The Instant
class stores the number of seconds elapsed since the start of the Unix epoch (1970-01-01T00:00:00Z). The Instant
class is suitable for storing a log of application events in a file as timestamp values. The ZoneId
and ZoneOffset
classes are related to time zones and hence are unrelated to storing timestamp values. The Duration
class is for time-based values in terms of quantity of time (such as seconds, minutes, and hours). The Period
class is for date-based values such as years, months, and days. D. The code segment prints: +08:00
Given a ZonedDateTime
object, the getOffset()
method returns a ZoneOffset
object that corresponds to the offset of the time zone from UTC/Greenwich. Given that the time-offset value for the Asia/Singapore zone from UTC/Greenwich is +08:00, the toString()
method of ZoneOffset
prints the string “+08:00” to the console. E. This code segment throws java.time.DateTimeException
with the message "Invalid date 'FEBRUARY 31'"
. The date value 31 passed in the call LocalDate.of(2015, 2,
31);
is invalid for the month February, and hence the of()
method in the LocalDate
class throws DateTimeException
. One of the predefined values in DateTimeFormatter
is ISO_DATE
. Hence, it does not result in a compiler error for the statement marked with the comment DEF
. The statement marked with the comment USE
compiles without errors because it is the correct way to use the format()
method in the DateTimeFormatter
class. B. Friday E
is the day name in the week; the pattern "EEEE"
prints the name of the day in its full format. “Fri” is a short form that would be printed by the pattern "E"
, but "EEEE
" prints the day of the week in full form: for example, “Friday”. Because the locale is Locale.US
, the result is printed in English. The output “Sept” or “September” is impossible because E
refers to the name in the week, not in a month.
九、Java I/O 基础知识
认证目标 |
---|
从控制台读取和写入数据 |
在 java.io 包中使用 BufferedReader、BufferedWriter、File、FileReader、FileWriter、FileInputStream、FileOutputStream、ObjectOutputStream、ObjectInputStream 和 PrintWriter |
在本章中,我们将向您介绍 Java I/O 编程的基础知识。我们将讨论两个主题:如何从控制台读写数据,然后如何使用(文件)流读写数据。
java.io 和 java.nio 包中提供了对文件操作的支持。在本章的开始部分,我们将只关注 java.io 包;稍后,我们将关注使用流读写数据,但不关注 java.io 包中提供的其他特性。java.nio 包为文件 I/O 提供了全面的支持,我们将在第十章中介绍。
从控制台读取和写入控制台
认证目标 |
---|
从控制台读取和写入数据 |
对于控制台的读写,可以使用标准的输入、输出和错误流,或者使用Console
类。现在让我们来讨论这两种方法。
了解标准流
java.lang.System
类中的公共静态字段in
、out
和err
分别代表标准输入、输出和错误流。System.in
为java.io.InputStream
型System.out
和System.err
为java.io.PrintStream
型。
下面是一个从控制台读取并打印一个整数的编程示例(清单 9-1 ):
Listing 9-1. Read.java
import java.io.IOException;
class Read {
public static void main(String []args) {
System.out.print("Type a character: ");
int val = 0;
try {
// the return type of read is int, but it returns a byte value!
val = System.in.read();
} catch(IOException ioe) {
System.err.println("Cannot read input " + ioe);
System.exit(-1);
}
System.out.println("You typed: " + val);
}
}
以下是该程序的运行示例:
D:\> $ java Read
Type a character: 5
You typed: 53
read
方法的返回类型是int
,但是它返回一个范围在 0 到 255 之间的byte
值(是的,它是不直观的)。因此,对于输入 5,程序打印其 ASCII 值 53。read
方法“阻塞”(即等待)用户输入;如果读取时发生 I/O 异常,该方法抛出一个IOException
。
这个程序演示了所有三个流的使用——System.in
用于从控制台获取输入,System.out
用于打印读取的整数值,System.err
用于在发生 I/O 异常时发出错误。
重载的read
方法本质上是低级的,以字节为单位工作。读取其他类型的输入,如Strings
需要与Reader
或Scanner
类一起使用,如:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();
// or use java.util.Scanner, as in:
Scanner scanner = new Scanner(System.in);
String str = scanner.next();
我们将在本章后面讨论更多关于Reader
和Scanner
类的内容。
重新分配标准流
标准流在 JVM 启动时初始化。有时,通过重新分配来重定向标准流是很有用的。方法System.setIn
接受一个InputStream
对象,方法System.setOut
和System.setError
接受PrintStream
对象作为参数。下面是一个代码片段,它通过将System.out
流重新分配给一个输出文本文件,将标准输出捕获到一个文件中:
import java.io.*;
class StreamTest {
public static void main(String []args ){
try{
PrintStream ps = new PrintStream("log.txt");
System.setOut(ps);
System.out.println("Test output to System.out");
} catch(Exception ee){
ee.printStackTrace();
}
}
}
当您执行这段代码时,程序将创建一个名为“log.txt”的文件,并在该文件中打印字符串“Test output to System.out”。
重定向流在许多情况下都很有用。例如,为了进行测试,您可能希望从文本文件中读取输入,而不是从控制台中读取。您可以通过将标准输入流重定向到文本文件来实现这一点。同样,您可能希望将错误流重新分配给文本文件,以便将所有错误消息存储在日志文件中。您可以通过调用System.setErr
方法来实现这一点。
了解控制台类
使用Console
类有助于从控制台读取数据和将数据写入控制台。注意,这里的“控制台”一词指的是字符输入设备(通常是键盘)和字符显示设备(通常是屏幕显示器)。您可以使用System.console()
方法获得对控制台的引用;如果 JVM 不与任何控制台相关联,这个方法将返回null
。
您的第一个练习是实现一个简单的Echo
命令,当您运行这个程序时,它打印出作为输入的文本行(清单 9-2 )。
Listing 9-2. Echo.java
import java.io.Console;
// simple implementation of Echo command
class Echo {
public static void main(String []args) {
// get the System console object
Console console = System.console();
if(console == null) {
System.err.println("Cannot retrieve console object - are you running your application from an IDE? Exiting the application … ");
System.exit(-1); // terminate the application
}
// read a line and print it through printf
console.printf(console.readLine());
}
}
下面是程序对于不同输出的行为(在第一次运行中,我们键入“hello world”作为输入,在第二次运行中,我们终止程序):
D:\>java Echo
hello world
hello world
D:\>java Echo
^Z
Exception in thread "main" java.lang.NullPointerException
at java.util.regex.Matcher.getTextLength(Matcher.java:1234)
… [this part of the stack trace elided to save space]
at Echo.main(Echo.java:14)
对于普通的文本输入,这个程序工作得很好。如果您键入 no input 并尝试用^z 或^d (Ctrl+Z 或 Ctrl+D 组合键)终止程序,那么程序不会收到任何输入,因此readLine()
方法返回 null 当printf
接受一个空参数时,它抛出一个NullReferenceException
。
请注意,您是从命令行运行这个程序的。如果从命令行调用 JVM 而不重定向输入或输出流,方法System.console()
将成功,因为 JVM 将与控制台(通常是键盘和显示屏)相关联。如果 JVM 是由 IDE 间接调用的,或者如果 JVM 是从后台进程调用的,那么方法调用System.console()
将失败并返回null
。例如,如果您从 IntelliJ IDEA 或 Eclipse IDEs 运行,System.console()
将通过返回 null 而失败。
如果 JVM 是由 IDE 间接调用的,或者如果 JVM 是从后台进程调用的,那么方法调用
System.console()
将失败并返回 null。
表 9-1 中列出了Console
类中可用的一些重要方法。
表 9-1。
Important Methods in the Console Class
方法 | 简短描述 |
---|---|
Reader reader() |
返回与此Console 对象相关联的Reader 对象;可以通过这个返回的引用执行读取操作。 |
PrintWriter writer() |
返回与此Console 对象相关联的PrintWriter 对象;可以通过这个返回的引用执行写操作。 |
String readLine() |
读取一行文本String (这个返回的 string 对象不包含任何行终止字符);如果失败,则返回 null(例如,用户在控制台中按下了 Ctrl+Z 或 Ctrl+D) |
String readLine(String fmt, Object… args) |
与readLine() 方法相同,但是它首先打印字符串fmt 。 |
char[] readPassword() |
读取密码文本并以 char 数组的形式返回;此方法禁用回显,因此当用户键入密码时,控制台中不会显示任何内容。 |
char[] readPassword(String fmt, Object… args) |
与readPassword() 方法相同,但是在读取密码字符串之前,它首先打印作为格式字符串参数给出的字符串。 |
Console format(String fmt, Object… args) |
将格式化字符串(基于fmt 字符串和传递的args 的值创建)写入控制台。 |
Console printf(String fmt, Object… args) |
将格式化字符串(基于fmt 字符串和传递的args 的值创建)写入控制台。这个printf 方法与format 方法相同:这是一个“方便的方法”——方法printf 和格式说明符为大多数 C/C++程序员所熟悉,因此除了 format 方法之外还提供了这个方法。 |
void flush() |
刷新控制台对象缓冲区中仍待打印的任何数据。 |
使用控制台类格式化输出
Console
类支持方法printf()
和format()
中的格式化 I/O,加上重载方法readPassword()
和readLine()
。我们现在将讨论使用方法printf()
(以及类似的format()
方法)的格式化输出,稍后讨论readPassword()
和readLine()
方法。
方法printf()
使用字符串格式化标志来格式化字符串。它非常类似于 C 编程语言库中提供的printf()
函数。printf()
方法的第一个参数是一个格式字符串。格式字符串可以包含字符串文字和格式说明符。实际参数在格式字符串之后传递。如果传递的格式不正确,这个方法会抛出IllegalFormatException
。
格式说明符是字符串格式概念的关键。它们定义了特定数据类型及其格式(如对齐和宽度)的占位符。printf()
方法的其余参数是变量(或文字),它们提供实际数据来填充格式说明符序列中的占位符。
让我们讨论一个何时以及为什么需要使用格式说明符的详细例子。假设您想打印一个足球运动员的表格,其中包含他们的姓名、比赛场次、得分和每场比赛的进球数信息。但是,有一些限制:
- 你想把玩家的名字印在左边(左对齐)。
- 您需要为玩家的名字指定至少 15 个字符。
- 您希望在距制表位一定距离处打印每一列。
- 您希望在每场比赛的进球数信息中只指定一个精确点。
清单 9-3 展示了如何实现这一点。
Listing 9-3. FormattedTable.java
// This program demonstrates the use of format specifiers in printf
import java.io.Console;
class FormattedTable {
void line(Console console) {
console.printf("------------------------------------------------------------\n");
}
void printHeader(Console console) {
console.printf("%-15s \t %s \t %s \t %s \n", "Player", "Matches", "Goals", "Goals per match");
}
void printRow(Console console, String player, int matches, int goals) {
console.printf("%-15s \t %5d \t\t %d \t\t %.1f \n", player, matches, goals,
((float)goals/(float)matches));
}
public static void main(String[] str) {
FormattedTable formattedTable = new FormattedTable();
Console console = System.console();
if(console != null) {
formattedTable.line(console);
formattedTable.printHeader(console);
formattedTable.line(console);
formattedTable.printRow(console, "Demando", 100, 122);
formattedTable.printRow(console, "Mushi", 80, 100);
formattedTable.printRow(console, "Peale", 150, 180);
formattedTable.line(console);
}
}
}
该程序产生以下输出:
-----------------------------------------------------------------------------------
Player Matches Goals Goals per match
-----------------------------------------------------------------------------------
Demando 100 122 1.2
Mushi 80 100 1.3
Peale 150 180 1.2
-----------------------------------------------------------------------------------
让我们分析一下在printRow()
方法- "%-15s \t %5d \t\t %d \t\t %.1f \n"
中指定的格式字符串
- 格式字符串的第一部分是
"%-15s"
。这里,表达式以%
开始,这表示格式说明符字符串的开始。 - 下一个符号是
'-'
,用来使字符串左对齐。数字"15"
指定字符串的宽度,最后"s"
的数据类型说明符指示输入数据类型为String
。 - 下一个格式说明符字符串是
"%5d"
,表示它需要一个最少显示 5 位数的整数。 - 最后一个格式说明符字符串是
"%.1f"
,它需要一个浮点数,显示一个精确数字。 - 所有格式说明符字符串都用一个或多个
"\t"s
(制表位)分隔,以便在列之间留出空间。
现在让我们讨论一下printf()
方法中格式说明符的模板:
%[argument_index][flags][width][.precision]datatype_specifier
-
正如您所看到的,每个格式说明符都以
%
符号开始,后跟参数索引、标志、宽度和精度信息,并以数据类型说明符结束。在这个字符串中,参数索引、标志、宽度和精度信息是可选的,而%
符号和数据类型说明符是必需的。 -
参数索引是指参数在参数列表中的位置;它是一个后跟\(的整数,如 1\)和 2$分别代表第一个和第二个参数。
-
标志是指定对齐和填充字符等特征的单字符符号。例如,标志
"-"
指定左对齐,而"0"
用前导零填充数字。 -
宽度说明符指示最终格式化字符串中将跨越的最小字符数。如果输入数据短于指定的宽度,则默认情况下会用空格填充。如果输入数据大于指定的宽度,则完整的数据会显示在输出中,而不会被修剪。
-
精度字段指定输出中的精度位数。这个可选字段对于浮点数特别有用。
-
Finally, the data type specifier indicates the type of expected input data. The field is a placeholder for the specified input data. Table 9-2 provides a list of commonly used data type specifiers.
表 9-2。
Commonly Used Data Type Specifiers
标志 描述 %b 布尔代数学体系的 %c 性格;角色;字母 %d 十进制整数(带符号) %e 科学格式的浮点数 %f 十进制格式的浮点数 %g 十进制或科学格式的浮点数(取决于作为参数传递的值) %h 传递的参数的 Hashcode %n 行分隔符(换行符) %o 格式化为八进制值的整数 %s 线 %t 日期/时间 %x 格式化为十六进制值的整数
注意,关于printf()
的讨论适用于Console
类中的format()
方法。实际上,printf
只是在内部调用了format
方法:
// code from java.io.Console.java
public Console printf(String format, Object … args) {
return format(format, args);
}
要记住的要点
以下几点可能对你的 OCPJP 八级考试有用:
- 如果没有指定任何字符串格式说明符,
printf()
方法将不会打印给定参数中的任何内容! - 只有在使用格式说明符字符串指定宽度时,
"-"
和0"
这样的标志才有意义。 - 也可以打印格式字符串中的
%
字符;然而,你需要为它使用一个转义序列。在格式说明符字符串中,%
是一个转义字符,这意味着您需要使用%%
来打印单个%
。 - 您可以使用参数索引功能(一个整数值后跟一个\(符号)通过索引位置显式引用参数。例如,以下打印“world hello”,因为参数的顺序颠倒了:`console.printf("%2\)s %1$s %n", "hello", "world");
- 格式字符串中的 console 是有效的
Console
对象,下面的代码段打印“10 a 12”:console.printf("%d %<x %<o", 10);``// 10 – the decimal value, a – the hexadecimal value of 10, and``// 12 – the octal value of 10
- 如果您没有提供格式字符串所期望的输入数据类型,那么您可以得到一个
IllegalFormatConversionException
。例如,如果您在您的printRow()
方法实现中提供一个字符串而不是一个期望的整数,您将得到下面的异常:Exception in thread "main" java.util.IllegalFormatConversionException:
d != java.lang.String
使用控制台类获取输入
您可以使用Console
类中提供的重载方法readPassword()
和readLine()
从控制台获取输入。在这些方法中,第一个参数是格式说明符字符串,后面的参数是将传递给格式说明符字符串的值。这两个方法返回从控制台读取的字符数据。readLine()
和readPassword()
方法有什么区别?主要区别在于readPassword()
不在控制台中显示输入的字符串(明显的原因是不显示密码),而readLine()
显示您在控制台中输入的内容。另一个微小的区别是readLine()
方法返回一个String
,而readPassword()
返回一个char
数组(参见清单 9-4 )。
Listing 9-4. Login.java
import java.io.Console;
import java.util.Arrays;
// code to illustrate the use of readPassword method
class Login {
public static void main(String []args) {
Console console = System.console();
if(console != null) {
String userName = null;
char[] password = null;
userName = console.readLine("Enter your username: ");
// typed characters for password will not be displayed in the screen
password = console.readPassword("Enter password: ");
// password is a char[]: convert it to a String first
// before comparing contents
if(userName.equals("scrat") && new String(password).equals("nuts")) {
// we're hardcoding username and password here for
// illustration, don't do such hardcoding in pratice!
console.printf("login successful!");
}
else {
console.printf("wrong user name or password");
}
// "empty" the password since its use is over
Arrays.fill(password, ' ');
}
}
}
下面是运行该程序的一个实例,键入正确的用户名和密码:
D:\>java Login
Enter your username: scrat
Enter password:
login successful!
请注意,键入密码时,控制台中没有显示任何内容。为什么这个程序中提供了语句Arrays.fill(password, ' ');
?建议在读取密码字符串使用完毕后将其“清空”;这里你使用Array
的fill()
方法来达到这个目的。这是一种安全的编程实践,可以避免恶意读取程序数据来发现密码字符串。事实上,与返回一个String
的readLine()
方法不同,readPassword()
方法返回一个 char 数组。使用 char 数组,一旦验证了密码,就可以清空它并从内存中删除密码文本的痕迹;对于垃圾回收的String
对象,这不像使用 char 数组那么简单。
使用流读写文件
认证目标 |
---|
在 java.io 包中使用 BufferedReader、BufferedWriter、File、FileReader、FileWriter、FileInputStream、FileOutputStream、ObjectOutputStream、ObjectInputStream 和 PrintWriter |
什么是流?流是有序的数据序列。Java 根据流来处理输入和输出。例如,当你从一个二进制文件中读取一个字节序列时,你是从一个输入流中读取;类似地,当您将一个字节序列写入一个二进制文件时,您正在写入一个输出流。请注意我们是如何从二进制文件中读取或写入字节的,但是从文本文件中读取或写入字符呢?与其他语言和操作系统类似,Java 在处理文本和二进制数据方面有所不同。在深入研究流和从文件中读取或写入数据之前,您必须首先理解字符流和字节流之间的区别,这对于理解本章的其余部分是必不可少的。
Java 8 中引入的 streams API(在第六章中介绍)不同于我们在本章中讨论的 I/O 流。
字符流和字节流
考虑一下 Java 源文件和编译器生成的类文件之间的区别。Java 源文件的扩展名为“”。java ”,旨在供人类以及编译器等编程工具阅读。但是,Java 类文件的扩展名为“”。类”并且不打算被人类阅读;它们应该由低级工具处理,比如 JVM(Windows 中的可执行 java.exe)和 Java 反汇编程序(Windows 中的可执行 javap.exe)。
A text file is a human-readable file containing text (or characters); Binary files are machine-readable or low-level data storage.
自然,你如何解释文本文件和二进制文件是不同的。例如,在文本文件中,您可以解释从文件中读取的数据,并区分制表符、空白字符、换行符等。然而,你不能像那样处理来自二进制文件的数据;都是低级的价值观。再举一个例子,考虑一个用文本编辑器(如 Windows 中的记事本)创建的. txt 文件;它包含人类可读的文本。现在,考虑把你的照片存储在. bmp 或者。jpeg 文件;这些文件肯定不是人类可读的。它们旨在通过照片编辑或图像处理软件进行处理,文件包含一些预先确定的低级格式的数据。
java.io
包有支持字符流和字节流的类。您可以将字符流用于基于文本的 I/O。字节流用于基于数据的 I/O。用于读取和写入的字符流分别称为读取器和写入器(由抽象类 Reader 和 Writer 表示)。用于读写的字节流分别称为输入流和输出流(由 InputStream 和 OutputStream 的抽象类表示)。表 9-3 总结了字符流和字节流的区别,供你快速参考。
表 9-3。
Differences Between Character Streams and Byte Streams
字符流 | 字节流 |
---|---|
用于读写基于字符或文本的 I/O,如文本文件、文本文档、XML 和 HTML 文件。 | 用于读取或写入二进制数据 I/O,如可执行文件、映像文件和低级文件格式的文件,如.zip 、.class 、.obj 和.exe 。 |
处理的数据是 16 位 Unicode 字符。 | 处理的数据是字节(即 8 位数据单位)。 |
输入和输出字符流分别称为读取器和写入器。 | 输入和输出字节流分别简称为输入流和输出流。 |
java.io 包中的Reader 和Writer 的抽象类及其派生类提供了对字符流的支持。 |
InputStream 和OutputStream 的抽象类以及它们在java.io 包中的派生类提供了对字节流的支持。 |
如果你试图在需要字符流的时候使用字节流,反之亦然,你会在你的程序中得到一个讨厌的惊喜。例如,位图(.bmp
)图像文件必须使用字节流进行处理;如果你尝试使用字符流,你的程序将无法工作。所以不要混淆溪流!
字符流
在本节中,您将探索字符流的 I/O。您将学习如何读写文本文件,以及一些可选特性,如缓冲以加速 I/O。对于读写文本文件,您可以分别使用从Reader
和Writer
抽象类派生的类。对于角色流,图 9-1 显示了重要的Reader
类,表 9-4 提供了这些类的简短描述。图 9-2 显示了重要的Writer
类,表 9-5 提供了这些类的简短描述。请注意,在这一章中,我们将只涉及这个类层次中的几个重要的类。
表 9-5。
Important Classes Deriving from the Writer Class
类别名 | 简短描述 |
---|---|
StringWriter |
在字符串缓冲区中收集输出的字符流,可用于创建string 。 |
OutputStreamWriter |
这个类是字符流和字节流之间的桥梁。 |
FileWriter |
为编写字符文件提供支持的OutputStreamWriter 的派生类。 |
PipedWriter |
PipedReader 和PipedWriter 类形成一对,用于“管道化”字符流中字符的读/写。 |
FilterWriter |
流的抽象基类,支持将数据作为字符写入字符流时对数据应用的过滤操作。 |
PrintWriter |
支持将字符格式化打印到输出字符流。 |
BufferedWriter |
向基础字符流添加缓冲,这样就不需要为每个读写操作访问基础文件系统。 |
图 9-2。
Important classes deriving from the Writer class
表 9-4。
Important Classes Deriving from the Reader Class
类别名 | 简短描述 |
---|---|
StringReader |
在string 上操作的字符流。 |
InputStreamReader |
这个类是字符流和字节流之间的桥梁。 |
FileReader |
为读取字符文件提供支持的InputStreamReader 的派生类。 |
PipedReader |
PipedReader 和PipedWriter 类形成一对“管道化”的字符读/写。 |
FilterReader |
流的抽象基类,支持在从流中读取字符时对数据应用过滤操作。 |
PushbackReader |
FilterReader 的派生类,允许将读取的字符推回到流中。 |
BufferedReader |
向基础字符流添加缓冲,这样就不需要为每个读写操作访问基础文件系统。 |
LineNumberReader |
从底层字符流中读取字符时跟踪行号的BufferedReader 的派生类。 |
图 9-1。
Important classes deriving from the Reader class
读取文本文件
类读取流中的内容,并尝试将它们解释为字符,如制表符、文件尾和换行符。清单 9-5 实现了 Windows 中type
命令的简化版本(Linux/Unix/Mac 中类似的命令是cat
命令)。type
命令显示作为命令行参数传递的文件内容。
Listing 9-5. Type.java
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
// implements a simplified version of "type" command provided in Windows given
// a text file name(s) as argument, it prints the content of the text file(s) on console
class Type {
public static void main(String []files) {
if(files.length == 0) {
System.err.println("pass the name of the file(s) as argument");
System.exit(-1);
}
// process each file passed as argument
for(String file : files) {
// try opening the file with FileReader
try (FileReader inputFile = new FileReader(file)) {
int ch = 0;
// while there are characters to fetch, read, and print the
// characters when EOF is reached, read() will return -1,
// terminating the loop
while( (ch = inputFile.read()) != -1) {
// ch is of type int - convert it back to char
// before printing
System.out.print( (char)ch );
}
} catch (FileNotFoundException fnfe) {
// the passed file is not found …
System.err.printf("Cannot open the given file %s ", file);
}
catch(IOException ioe) {
// some IO error occurred when reading the file …
System.err.printf("Error when processing file %s… skipping it", file);
}
// try-with-resources will automatically release FileReader object
}
}
}
对于一个示例文本文件,下面是 Windows 中的type
命令和我们的Type
程序的输出:
D:\> type SaturnMoons.txt
Saturn has numerous icy moons in its rings. Few large moons of Saturn are - Mimas, Enceladus, Tethys, Dione, Rhea, Titan, Iapetus, and Hyperion.
D:\> java Type SaturnMoons.txt
Saturn has numerous icy moons in its rings. Few large moons of Saturn are - Mimas, Enceladus, Tethys, Dione, Rhea, Titan, Iapetus, and Hyperion.
它像预期的那样工作。在这个程序中,您正在实例化FileReader
类,并传递要打开的文件的名称。如果没有找到文件,FileReader 构造函数将抛出一个FileNotFoundException
。
文件打开后,使用read()
方法获取底层文件中的字符。你在一个字符一个字符地阅读。或者,可以使用readLine()
等方法逐行读取。
注意,read()
方法返回一个 int 而不是一个char
——这是因为当read()
到达文件尾(EOF)时,它返回-1,这在 char 的范围之外。因此,read()
方法返回一个 int 来表示已经到达了文件的末尾,您应该停止尝试从底层流中读取更多的字符。
在这个程序中,你只读取一个文本文件;现在,您将尝试读取和写入一个文本文件。
读取和写入文本文件
在前面读取文本文件的例子(清单 9-5 )中,您创建了如下的字符流:
FileReader inputFile = new FileReader(file);
这使用非缓冲 I/O,与缓冲 I/O 相比效率较低。换句话说,直接传递读取字符,而不是使用临时(内部)缓冲区,这将加速 I/O。要以编程方式使用缓冲 I/O,您可以将FileReader
引用传递给BufferedReader
对象,如下所示:
BufferedReader inputFile = new BufferedReader(new FileReader(file));
同理,也可以使用BufferedWriter
进行缓冲输出。(在字节流的情况下,你可以使用BufferedInputStream
和BufferedOutputStream
,我们将在本章后面讨论)。
现在,您将使用缓冲 I/O 来读取和写入文本文件。清单 9-6 包含了 Windows 中copy
命令的简化版本。
Listing 9-6. Copy.java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
// implements a simplified version of "copy" command provided in Windows
// syntax: java Copy SrcFile DstFile
// copies ScrFile to DstFile; over-writes the DstFile if it already exits
class Copy {
public static void main(String []files) {
if(files.length != 2) {
System.err.println("Incorrect syntax. Correct syntax: Copy SrcFile DstFile");
System.exit(-1);
}
String srcFile = files[0];
String dstFile = files[1];
// try opening the source and destination file
// with FileReader and FileWriter
try (BufferedReader inputFile = new BufferedReader(new FileReader(srcFile));
BufferedWriter outputFile = new BufferedWriter(new FileWriter(dstFile))) {
int ch = 0;
// while there are characters to fetch, read the characters from
// source stream and write them to the destination stream
while( (ch = inputFile.read()) != -1) {
// ch is of type int - convert it back to char before
// writing it
outputFile.write( (char)ch );
}
// no need to call flush explicitly for outputFile - the close()
// method will first call flush before closing the outputFile stream
} catch (FileNotFoundException fnfe) {
// the passed file is not found …
System.err.println("Cannot open the file " + fnfe.getMessage());
}
catch(IOException ioe) {
// some IO error occurred when reading the file …
System.err.printf("Error when processing file; exiting … ");
}
// try-with-resources will automatically release FileReader object
}
}
我们先检查一下这个程序是否有效。将这个 Java 源程序本身(Copy.java
)复制到另一个文件(DuplicateCopy.java
)。您可以使用 Windows 中提供的fc
(文件比较)命令(或 Linux/Unix/Mac 中的diff
命令)来确保原始文件和复制文件的内容是相同的,以确保程序正确运行。
D:\> java Copy Copy.java DuplicateCopy.java
D:\> fc Copy.java DuplicateCopyjava
Comparing files Copy.java and DuplicateCopy.java
FC: no differences encountered
是的,它工作正常。如果给它一个不存在的源文件名呢?
D:\> java Copy Cpy.java DuplicateCopyjava
Cannot open the file Cpy.java (The system cannot find the file specified)
您键入了Cpy.java
而不是Copy.java
,程序终止,并显示一条可读的错误消息,这是意料之中的。
这个程序是这样工作的。在 try-with-resources 语句中,您打开了srcFile
进行读取,打开了dstFile
进行写入。您想使用缓冲的 I/O,所以您将FileReader
和FileWriter
分别传递给了BufferedReader
和BufferedWriter
。
try (BufferedReader inputFile = new BufferedReader(new FileReader(srcFile));
BufferedWriter outputFile = new BufferedWriter(new FileWriter(dstFile)))
您正在使用 try-with-resources 语句(在第七章的中讨论过),在关闭流之前,BufferedWriter
的close()
方法将首先调用flush()
方法。
当你在程序中使用缓冲的 I/O 时,在你想确保所有未决的字符或数据都被刷新(即写入底层文件)的地方显式调用
flush()
方法是个好主意。
“标记化”文本
在最后两个例子中(清单 9-5 和 9-6 ,您只是读取或写入文本文件。但是,在现实世界的程序中,您可能希望在读取或写入文件时执行一些处理。例如,您可能希望寻找某些模式,搜索某些特定的字符串,用一个字符序列替换另一个字符序列,过滤掉特定的单词,或者以某种方式格式化输出。为此,您可以使用现有的 API,比如正则表达式和Scanner
。
举例来说,假设您想要列出给定文本文件中的所有单词,并删除所有不必要的空格、标点符号等。此外,您需要按字母顺序打印结果单词。要解决这个问题,您可以使用一个Scanner
并传递您想要匹配或定界的正则表达式(参见清单 9-7 )。
Listing 9-7. Tokenize.java
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
// read the input file and convert it into "tokens" of words;
// convert the words to same case (lower case), remove duplicates, and print the words
class Tokenize {
public static void main(String []args) {
// read the input file
if(args.length != 1) {
System.err.println("pass the name of the file to be read as an argument");
System.exit(-1);
}
String fileName = args[0];
// use a TreeSet<String> which will automatically sort the words
// in alphabetical order
Set<String> words = new TreeSet<>();
try ( Scanner tokenizingScanner = new Scanner(new FileReader(fileName)) ) {
// set the delimiter for text as non-words (special characters,
// white-spaces, etc), meaning that all words other than punctuation
// characters, and white-spaces will be returned
tokenizingScanner.useDelimiter("\\W");
while(tokenizingScanner.hasNext()) {
String word = tokenizingScanner.next();
if(!word.equals("")) { // process only non-empty strings
// convert to lowercase and then add to the set
words.add(word.toLowerCase());
}
}
// now words are in alphabetical order without duplicates,
// print the words separating them with tabs
for(String word : words) {
System.out.print(word + '\t');
}
} catch (FileNotFoundException fnfe) {
System.err.println("Cannot read the input file - pass a valid file name");
}
}
}
让我们看看它是否有效:
D:\> type limerick.txt
There was a young lady of Niger
Who smiled as she rode on a tiger.
They returned from the ride
With the lady inside
And a smile on the face of the tiger.
D:\> java Tokenize limerick.txt
a and as face from inside lady niger of on returned ride
rode she smile smiled the there they tiger was who with young
是的,它确实工作正常。现在让我们看看这个程序做什么。程序首先使用一个FileReader
打开文件,并将其传递给Scanner
对象。程序用useDelimiter("\\W");
为Scanner
设置分隔符,非单词的“\W”匹配,所以任何非单词字符都将成为分隔符。(注意,您正在设置分隔符,而不是您想要匹配的模式)。该程序利用一个TreeSet<String>
来存储读取的字符串。程序从底层流中读取单词,检查它是否为非空字符串,并将字符串的小写版本添加到TreeSet
。由于数据结构是一个TreeSet
,所以它删除了重复项(记住一个TreeSet
是-a Set
,它不允许重复)。此外,它还是一个有序的数据结构,这意味着它维护了插入值的“排序”,在本例中是按字母顺序排列的Strings
。因此,程序正确地打印出包含一首打油诗的给定文本文件中的单词。
字节流
在本节中,您将探索字节流的 I/O。您将首先学习如何读写数据文件,以及如何对对象进行流式处理,将它们存储在文件中,然后读回它们。OutputStream
的类及其派生类如图 9-3 所示;InputStream
及其派生类如图 9-4 所示。
图 9-4。
Important classes deriving from the InputStream abstract class
图 9-3。
Important classes deriving from the OutputStream abstract class
表 9-6 总结了InputStream
和OutputStream
的重要类别。
表 9-6。
Important Classes Deriving from the InputStream and OutputStream Classes
类别名 | 简短描述 |
---|---|
PipedInputStream, PipedOutputStream |
PipedInputStream 和PipedOutputStream 创建可发送和接收数据的通信通道。PipedOutputStream 发送数据,PipedInputStream 接收信道上发送的数据。 |
FileInputStream, FileOutputStream |
FileInputStream 从文件中接收字节流,FileOutputStream 将字节流写入文件。 |
FilterInputStream, FilterOutputStream |
这些过滤后的流用于向普通流添加功能。使用FilterInputStream 可以过滤InputStream 的输出。使用FilterOutputStream 可以过滤OutputStream 的输出。 |
BufferedInputStream, BufferedOutputStream |
向输入流添加缓冲能力。BufferedOutputStream 给输出流增加缓冲能力。 |
PushbackInputStream |
作为FilterInputStream 的子类,它为输入流增加了“推回”功能。 |
DataInputStream, DataOutputStream |
DataInputStream 可用于从输入流中读取 java 原始数据类型。DataOutputStream 可用于将 Java 原始数据类型写入输出流。 |
读取字节流
字节流用于处理不包含人类可读文本的文件。例如,一个 Java 源文件有人类可读的内容,但是一个".class"
文件没有。一个".class"
文件意味着由 JVM 处理,因此您必须使用字节流来处理".class"
文件。
一个".class"
文件的内容以特定的文件格式编写,在 Java 虚拟机(JVM)的规范中有描述。不用担心;您不会理解这种复杂的文件格式,但您只需检查它的“幻数”每种文件格式都有一个神奇的数字,用于快速检查文件格式。比如”。MZ”是 Windows 中.exe
文件的幻数(或者更确切地说,是幻串)。类似地,".class"
文件有一个幻数"0xCAFEBABE"
,写为十六进制值。这些幻数通常被写成可变长度文件格式的前几个字节。
为了理解字节流是如何工作的,您只需检查给定的文件是否以幻数“0xCAFEBABE”开头(清单 9-8 )。如果是这样,它可能是一个有效的".class"
文件;如果不是,那肯定不是".class"
文件。
Listing 9-8. ClassFileMagicNumberChecker.java
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
// check if the passed file is a valid .class file or not.
// note that this is an elementary version of a checker that checks if the given file
// is a valid file that is written according to the JVM specification
// it checks only the magic number
class ClassFileMagicNumberChecker {
public static void main(String []args) {
if(args.length != 1) {
System.err.println("Pass a valid file name as argument");
System.exit(-1);
}
String fileName = args[0];
// create a magicNumber byte array with values for four bytes in 0xCAFEBABE
// you need to have an explicit down cast to byte since
// the hex values like 0xCA are of type int
byte []magicNumber = {(byte) 0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE};
try (FileInputStream fis = new FileInputStream(fileName)) {
// magic number is of 4 bytes –
// use a temporary buffer to read first four bytes
byte[] u4buffer = new byte[4];
// read a buffer full (4 bytes here) of data from the file
if(fis.read(u4buffer) != -1) { // if read was successful
// the overloaded method equals for two byte arrays
// checks for equality of contents
if(Arrays.equals(magicNumber, u4buffer)) {
System.out.printf("The magic number for passed file %s matches that of
a .class file", fileName);
}
else {
System.out.printf("The magic number for passed file %s does not match
that of a .class file", fileName);
}
}
} catch(FileNotFoundException fnfe) {
System.err.println("file does not exist with the given file name ");
} catch(IOException ioe) {
System.err.println("an I/O error occurred while processing the file");
}
}
}
我们先来看看通过传递源码(。java
)文件和".class"
文件为同一程序。
D:> java ClassFileMagicNumberChecker ClassFileMagicNumberChecker.java
The magic number for passed file ClassFileMagicNumberChecker.java does not match that of a .class file
D:\> java ClassFileMagicNumberChecker ClassFileMagicNumberChecker.class
The magic number for passed file ClassFileMagicNumberChecker.class matches that of a .class file
是的,它有效。类InputStream
和OutputStream
构成了字节流层次结构的基础。您执行文件 I/O,因此以一个FileInputStream
打开给定的文件。您需要检查前四个字节,所以在临时缓冲区中读取四个字节。您需要将该缓冲区的内容与字节序列 0xCA、0xFE、0xBA 和 0xBE 进行比较。如果这两个数组的内容不相等,那么传递的文件就不是一个".class"
文件。
在这个程序中,您使用一个FileInputStream
直接操纵底层字节流。如果在读取大量字节时需要加速程序,可以使用缓冲输出流,如
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
与这些输入流类似,您可以使用输出流将一个字节序列写入数据文件。你可以使用FileOutputStream
和BufferedOutputStream
来实现。
看了这个程序,你不觉得读取一个四字节的数组,比较字节数组的内容很别扭(而不是直接比较一个整数的内容)吗?换句话说,0xCAFEBABE
是一个整数值,您可以将这个值作为一个整数值直接读取,并与读取的整数值进行比较。为此,您需要使用数据流,它提供了像readInt()
这样的方法,我们现在将讨论这些方法。
数据流
为了理解如何用字节流写或读,让我们写一个简单的程序,该程序将常量值写到数据文件中,然后从数据文件中读取常量值(参见清单 9-9 )。为了使问题简单,您将只以下列原始类型值的形式编写 0 到 9 的值:byte
、short
、int
、long
、float
和double
。
Listing 9-9. DataStreamExample.java
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
// A simple class to illustrate data streams; write constants 0 and 1 in different
// data type values into a file and read the results back and print them
class DataStreamExample {
public static void main(String []args) {
// write some data into a data file with hard-coded name "temp.data"
try (DataOutputStream dos =
new DataOutputStream(new FileOutputStream("temp.data"))) {
// write values 1 to 10 as byte, short, int, long, float and double
// omitting boolean type because an int value cannot
// be converted to boolean
for(int i = 0; i < 10; i++) {
dos.writeByte(i);
dos.writeShort(i);
dos.writeInt(i);
dos.writeLong(i);
dos.writeFloat(i);
dos.writeDouble(i);
}
} catch(FileNotFoundException fnfe) {
System.err.println("cannot create a file with the given file name ");
System.exit(-1); // don't proceed – exit the program
} catch(IOException ioe) {
System.err.println("an I/O error occurred while processing the file");
System.exit(-1); // don't proceed – exit the program
}
// the DataOutputStream will auto-close, so don't have to worry about it
// now, read the written data and print it to console
try (DataInputStream dis = new DataInputStream(new FileInputStream("temp.data"))) {
// the order of values to read is byte, short, int, long, float and
// double since we've written from 0 to 10,
// the for loop has to run 10 times
for(int i = 0; i < 10; i++) {
// %d is for printing byte, short, int or long
// %f, %g, or %e is for printing float or double
// %n is for printing newline
System.out.printf("%d %d %d %d %g %g %n",
dis.readByte(),
dis.readShort(),
dis.readInt(),
dis.readLong(),
dis.readFloat(),
dis.readDouble());
}
} catch(FileNotFoundException fnfe) {
System.err.println("cannot create a file with the given file name ");
} catch(IOException ioe) {
System.err.println("an I/O error occurred while processing the file");
} // the DataOutputStream will auto-close, so don't have to worry about it
}
}
首先,让我们通过执行程序来看看它是否有效。
D:> java DataStreamExample
0 0 0 0 0.000000 0.000000
1 1 1 1 1.000000 1.000000
2 2 2 2 2.000000 2.000000
3 3 3 3 3.000000 3.000000
4 4 4 4 4.000000 4.000000
5 5 5 5 5.000000 5.000000
6 6 6 6 6.000000 6.000000
7 7 7 7 7.000000 7.000000
8 8 8 8 8.000000 8.000000
9 9 9 9 9.000000 9.000000
是的,它有效。如前所述,数据文件的内容是不可读的。在本例中,您将值 0 到 9 作为各种原始类型值写入名为temp.data
的临时文件 write 中。如果您尝试打开此数据文件并查看其内容,您将无法识别或理解其中包含的内容。以下是其内容的一个示例:
D:>type temp.data
☺ ☺ ☺ ☺?Ç ? ☻ ☻ ☻ ☻@ @
♥ ♥ ♥ ♥@@ ♦ ♦ ♦ ♦@Ç ♣ ♣ ♣ ♣@á @¶
@L A @
α@L A @
A @"
文件temp.data
的类型化内容看起来像垃圾值,因为像整数值 0 或 9 这样的原始类型值是以字节存储的。然而,Windows 中的type
命令(或者 Linux/Unix/Mac 中的cat
命令)试图将这些字节转换成人类可读的字符,因此输出没有任何意义。只有当我们知道存储在文件中的数据的格式并根据该格式读取它时,数据才有意义。
现在让我们回到程序上来,看看它是如何工作的。程序在运行程序的当前目录中使用名为temp.data
的硬编码文件写入数据文件。这个程序首先写入数据,因此它将文件作为输出流打开。第一个 try 块中的以下语句是什么意思?
DataOutputStream dos = new DataOutputStream(new FileOutputStream("temp.data"))
可以用OutputStream
和它的派生类FileOutputStream
直接执行二进制 I/O,但是要处理原始类型值之类的数据格式,就需要使用DataOutputStream
,它充当底层FileOutputStream
的包装器。所以,您在这里使用了DataOutputStream
,它提供了writeByte
和writeShort
等方法。您可以使用这些方法将基本类型值 0 到 9 写入数据文件。请注意,您不必显式关闭流,因为您在 try-with-resources 语句中打开了DataOutputStream
,因此将自动调用dos
引用上的close()
方法。close()
方法也刷新底层流;这个close()
方法也将关闭对FileOutputStream
的底层引用。
一旦文件被写入,就可以用类似的方式读取数据文件。你打开一个FileInputStream
,用一个DataInputStream
把它包起来。您从流中读取数据,并在控制台中打印出来。您使用了格式说明符,如%d
(这是一种用于打印整型值(如byte
、short
、int
或long
)的通用格式说明符),以及用于打印类型为float
或double
的浮点值的%f
、%g
或%e
说明符;%n
用于打印换行符。
在这个程序中,您编写和读取了原始类型值。那么引用类型的对象呢,比如对象、地图等等?读写对象是通过对象流实现的,我们现在将讨论对象流。
写入和读取对象流
假设您正在创建一个在线电子商务网站。您可以选择将包含在对象中的数据(如客户和购买请求)写入 RDBMS(我们将在第十二章的中介绍 JDBC)。或者,您可以将对象直接存储在平面文件中,而不是将数据存储在 RDBMS 中:在这种情况下,您必须知道如何将对象读写到流中。类ObjectInputStream
和ObjectOutputStream
支持读写程序中使用的 Java 对象。
清单 9-10 包含了一个简单的例子,将Map
数据结构的内容写入一个文件,并读回它,以说明使用类ObjectInputStream
和ObjectOutputStream
来读取或写入对象。你把最近三任美国总统的详细资料储存在这张地图里。
Listing 9-10. ObjectStreamExample.java
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;
// A simple class to illustrate object streams: fill a data structure, write it to a
// temporary file and read it back and print the read data structure
class ObjectStreamExample {
public static void main(String []args) {
Map<String, String> presidentsOfUS = new HashMap<>();
presidentsOfUS.put("Barack Obama", "2009 to --, Democratic Party, 56th term");
presidentsOfUS.put("George W. Bush", "2001 to 2009, Republican Party, 54th and 55th terms");
presidentsOfUS.put("Bill Clinton", "1993 to 2001, Democratic Party, 52nd and 53rd terms");
try (ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("object.data"))) {
oos.writeObject(presidentsOfUS);
} catch(FileNotFoundException fnfe) {
System.err.println("cannot create a file with the given file name ");
} catch(IOException ioe) {
System.err.println("an I/O error occurred while processing the file");
} // the ObjectOutputStream will auto-close, so don't have to worry about it
try (ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("object.data"))) {
Object obj = ois.readObject();
// first check if obj is of type Map
if(obj != null && obj instanceof Map) {
Map<?, ?> presidents = (Map<?, ?>) obj;
System.out.println("President name \t Description");
for(Map.Entry<?, ?> president : presidents.entrySet()) {
System.out.printf("%s \t %s %n", president.getKey(),
president.getValue());
}
}
} catch(FileNotFoundException fnfe) {
System.err.println("cannot create a file with the given file name ");
} catch(IOException ioe) {
System.err.println("an I/O error occurred while processing the file");
} catch(ClassNotFoundException cnfe) {
System.err.println("cannot recognize the class of the object - is the file
corrupted?");
}
}
}
在讨论程序如何工作之前,我们先检查一下它是否工作。
D:\> java ObjectStreamExample
President name Description
Barack Obama 2009 to --, Democratic Party, 56th term
Bill Clinton 1993 to 2001, Democratic Party, 52nd and 53rd terms
George W. Bush 2001 to 2009, Republican Party, 54th and 55th terms
序列化过程使用内容描述(称为元数据)转换内存中对象的内容。当对象引用其他对象时,序列化机制也会将它们作为序列化字节的一部分。如果您尝试打开保存对象的文件,您将无法读取这些先序列化后保存的对象。例如,如果你试图读取object.data
文件,你会看到许多不可读的字符。
现在,让我们回到程序,看看它是如何工作的。在这个程序中,你用最近三任美国总统的详细信息填充HashMap
容器。然后,按如下方式打开输出流:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.data"))
FileOutputStream
在当前目录下打开一个名为object.data
的临时文件。ObjectOutputStream
是这个底层FileOutputStream
的包装器。在这个 try-with-resources 块中,只有一条语句oos.writeObject(presidentsOfUS)
,它将对象写入object.data
文件。
读取对象比编写对象需要更多的工作。ObjectInputStream
中的readObject()
方法返回一个Object
类型。你需要把它转换回Map<String, String>
。在将其向下转换为这个特定类型之前,您要检查obj
是否属于类型Map
。注意,您不必检查它是否是Map<String, String>
,因为这些泛型类型在称为type erasure
的过程中丢失了。因此,我们对泛型类型参数使用通配符,如:Map<?, ?> presidents = (Map<?, ?>) obj
。一旦向下转换成功,就可以读取该对象中内容的值。参见第四章泛型和集合中关于类型擦除和通配符的讨论。
要记住的要点
以下是值得注意的几点,可能对你参加 OCPJP 八级考试有所帮助:
- 当你使用缓冲流时,你应该在完成数据传输后调用
flush()
。内部缓冲区可能保存了一些数据,一旦您调用flush()
,这些数据将被清除并发送到目的地。但是,流上的方法close()
会自动调用flush()
。 - 您可能已经注意到您可以组合流对象。您可以创建一个接受
FileInputStream
对象的BufferedInputStream
对象。这样,一个流的输出被链接到过滤的流。这是以期望的方式定制流的重要、有用和漂亮的方式。 - 如果想自定义序列化的流程,可以实现
readObject()
和writeObject()
。请注意,这两个方法都是私有方法,这意味着您没有重写或重载这些方法。JVM 检查这些方法的实现,并调用它们而不是通常的方法。这听起来很奇怪,但这就是在 JVM 中实现序列化过程定制的方式。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
从控制台读取和写入数据
java.lang.System
类中的公共静态字段in
、out
和err
分别代表标准输入、输出和错误流。System.in
为java.io.InputStream
型System.out
和System.err
为java.io.PrintStream
型。- 您可以通过调用方法
System.setIn
、System.setOut
和System.setError
来重定向标准流。 - 您可以使用
System.console()
方法获得对控制台的引用;如果 JVM 不与任何控制台相关联,此方法将失败并返回 null。 Console-
支持格式化 I/O 中提供了许多方法。您可以使用Console
类中的printf()
和format()
方法来打印格式化文本;重载的readLine()
和readPassword()
方法将格式字符串作为参数。- 格式说明符的模板是:
%[flags][width][.precision]datatype_specifier
每个格式说明符以%符号开始,后面是标志、宽度和精度信息,以数据类型说明符结束。在格式字符串中,标志、宽度和精度信息是可选的,但%符号和数据类型说明符是必需的。 - 使用
readPassword()
方法读取安全字符串,如密码。建议使用Array
的fill()
方法将读入字符数组的密码“清空”(避免恶意访问键入的密码)。
在 java.io 包中使用 BufferedReader、BufferedWriter、File、FileReader、FileWriter、FileInputStream、FileOutputStream、ObjectOutputStream、ObjectInputStream 和 PrintWriter
- java.io 包中有支持字符流和字节流的类。
- 您可以将字符流用于基于文本的 I/O。字节流用于基于数据的 I/O。
- 用于读取和写入的字符流分别称为读取器和写入器(由抽象类 Reader 和 Writer 表示)。
- 用于读写的字节流分别称为输入流和输出流(由 InputStream 和 OutputStream 的抽象类表示)。
- 您应该只使用字符流来处理文本文件(或人类可读的文件),使用字节流来处理数据文件。如果你尝试使用一种类型的流而不是另一种,你的程序将不会像你预期的那样工作;即使它偶然工作,你也会得到讨厌的错误。所以不要混淆不同的流,为手头的特定任务使用正确的流。
- 对于字节流和字符流,都可以使用缓冲。缓冲区类是作为底层流的包装类提供的。在执行批量 I/O 操作时,使用缓冲可以加快 I/O 的速度。
- 对于处理具有原始数据类型和字符串的数据,可以使用数据流。
- 您可以使用对象流(ObjectInputStream 和 ObjectOutputStream 类)从内存中读取对象并将其写入文件,反之亦然。
Question TimeConsider the following code segment: OutputStream os = new FileOutputStream("log.txt");
System.setErr(new PrintStream(os)); // SET SYSTEM.ERR
System.err.println("Error");
Which one of the following statements is true regarding this code segment? The line with comment SET SYSTEM.ERR will not compile and will result in a compiler error. The line with comment SET SYSTEM.ERR will result in throwing a runtime exception since System.err
cannot be programmatically redirected. The program will print the text “Error” in console since System.err
by default sends the output to console. This code segment redirects the System.err
to the log.txt
file and will write the text “Error” to that file. Which one of the following options correctly reads a line of string from the console? BufferedReader br = new BufferedReader(System.in);
String str = br.readLine();
BufferedReader br =
new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();
InputStreamReader isr =
new InputStreamReader (new BufferedReader(System.in));
String str = isr.readLine();
String str = System.in.readLine();
String str;
System.in.scanf(str);
Consider the following code snippet: console.printf("%d %1$x %1$o", 16);
Assuming that console
is a valid Console
object, what will it print? This program crashes after throwing an IllegalFormatException
This program crashes after throwing ImproperFormatStringException
This program prints: 16 16 16 This program prints: 16 10 20 There are two kinds of streams in the java.io package: character streams (i.e., those deriving from Reader and Writer interfaces) and byte streams (i.e., those deriving from InputStream and OutputStream). Which of the following statements is true regarding the differences between these two kinds of streams? In character streams, data is handled in terms of bytes; in byte streams, data is handled in terms of Unicode characters. Character streams are suitable for reading or writing to files such as executable files, image files, and files in low-level file formats such as .zip, .class, and .jag. Byte streams are suitable for reading or writing to text-based I/O such as documents and text, XML, and HTML files. Byte streams are meant for handling binary data that is not human-readable; character streams are meant for human-readable characters. Consider the following code snippet: USPresident usPresident = new USPresident("Barack Obama", "2009 to --", 56);
try (ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("USPresident.data"))) {
oos.writeObject(usPresident);
usPresident.setTerm(57);
oos.writeObject(usPresident);
}
If you deserialize the object and print the field term
(term
is declared as int
and is not a transient
), what will it print? 56 57 null Compiler error Runtime exception Consider the following code segment: FileInputStream findings = new FileInputStream("log.txt");
DataInputStream dataStream = new DataInputStream(findings);
BufferedReader br = new BufferedReader(new InputStreamReader(dataStream));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
Which TWO options are true regarding this code segment? br.close()
statement will close only the BufferedReader
object, and findings
and dataStream
will remain unclosed. The br.close()
statement will close the BufferedReader
object and the underlying stream objects referred by findings
and dataStream
. The readLine()
method invoked in the statement br.readLine()
can throw an IOException
; if this exception is thrown, br.close()
will not be called, resulting in a resource leak. The readLine()
method invoked in the statement br.readLine()
can throw an IOException
; however, there will not be any resource leaks since Garbage Collector collects all resources. In this code segment, no exceptions can be thrown calling br.close()
, so there is no possibility of resource leaks.
答案:
D. This code segment redirects the System.err
to the log.txt
file and will write the text “Error” to that file. Note that you can redirect the System.err
programmatically using the setErr()
method. System.err
is of type PrintStream
, and the System.setErr()
method takes a PrintStream
as an argument. Once the error stream is set, all writes to System.err
will be redirected to it. Hence, this program will create log.txt
with the text “Error” in it. B. BufferedReader br =
new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();
This is the right way to read a line of a string from the console where you pass a System.in reference to InputStreamReader and pass the returning reference to BufferedReader. From the BufferedReader reference, you can call the readLine()
method to read the string from the console. D. This program prints: 16 10 20 In the format specifier, “1\(” refers to first argument, which is 16 in this printf statement. Hence “%1\)x” prints the hexadecimal value of 16, which is 10. Further, “%1$o” prints the octal value of 16, which is 20. Hence the output “16 10 20” from this program. D. Byte streams are meant for handling binary data that is not human readable; character streams are for human-readable characters. In character streams, data is handled in terms of Unicode characters, whereas in byte streams, data is handled in terms of bytes. Byte streams are suitable for reading or writing to files such as executable files, image files, and files in low-level file formats such as .zip, .class, and .jar. Character streams are suitable for reading or writing to text-based I/O such as documents and text, XML, and HTML files. A. 56 Yes, it will print 56 even though you changed the term using its setter to 57 and serialized again. At the time of serialization, JVM checks for the duplicate object; if an object is already serialized then JVM do not serialize the object again; instead, JVM stores a reference to the serialized object. Options B and C. The br.close()
statement will close the BufferedReader
object and the underlying stream objects referred to by findings
and dataStream
. The readLine()
method invoked in the statement br.readLine()
can throw an IOException
; if this exception is thrown, br.close()
will not be called, resulting in a resource leak. Note that Garbage Collector will only collect unreferenced memory resources; it is the programmer’s responsibility to ensure that all other resources such as stream objects are released.
十、Java 文件 NIO.2
认证目标 |
---|
使用路径接口操作文件和目录路径 |
使用 Files 类来检查、读取、删除、复制、移动和管理文件或目录的元数据 |
将流 API 与 NIO.2 一起使用 |
Java 提供了一组丰富的 API,可以用来操作文件和目录。Java 7 引入了一组称为 NIO.2 的 I/O API,提供了执行与文件系统相关的操作的便捷方式。在 Java 8 中,你可以在 NIO.2 中使用 Stream API(在第六章的中讨论过)
前一章介绍了 I/O 基础知识;您学习了如何从控制台读取和写入,以及如何使用流读取和写入文件。在本章中,您将学习如何使用Path
界面对文件和目录路径进行操作。您还将学习使用Files
类执行各种文件操作,比如创建、移动、复制和删除。最后,您将看到如何在 NIO.2 中使用流 API。本章使用了java.util.function
包中的函数式接口和java.util.stream
包中的流 API,我们假设您在阅读本章之前已经阅读了第三章、第四章、第五章和第六章。
我们给出了文件和目录路径,假设您使用的是 Windows 机器。如果您在 Linux、Mac OS 或任何其他平台上,您可能需要对路径名进行一些小的更改,以便程序能够在您的机器上运行。
使用路径接口
| 认证目标 | 使用路径接口操作文件和目录路径 |
文件系统通常形成一棵树。文件系统从包含文件和目录(目录在 Windows 中也称为文件夹)的根目录开始。每个目录又可以有子目录或保存文件。要定位一个文件,您只需要把从根目录到包含该文件的直接目录的目录放在一起,加上一个文件分隔符,后跟文件名。例如,如果myfile.txt
文件驻留在mydocs
目录中,而后者驻留在根目录C:\
中,那么文件的路径就是C:\mydocs\myfile.txt
。每个文件都有一个唯一的路径来定位它(除了符号链接)。
路径可以是从根元素开始的绝对路径(比如C:\mydocs\myfile.txt
)。另一方面,路径可以被指定为相对路径。当你试图编译一个 Java 程序时,你会写出类似于javac programFileName.jav
a;此示例指定了相对于当前选定目录的 Java 源文件路径,因此这是一个相对路径。您需要一个引用路径(比如本例中的当前目录路径)来解释相对路径。
在继续之前,我们先来谈谈符号链接。符号链接就像指向实际文件的指针或引用。一般来说,符号链接对应用是透明的,这意味着操作是直接在文件上执行的,而不是在链接上执行的(当然,特定于符号链接的操作除外)。
接口是路径的编程抽象。path 对象包含构成由Path
对象表示的文件/目录的完整路径的目录和文件的名称;Path
抽象提供了提取路径元素、操作它们和添加它们的方法。稍后您会看到,几乎所有访问文件/目录以获取有关它们的信息或操纵它们的方法都使用了Path
对象。表 10-1 总结了该接口中的重要方法。
表 10-1。
Important Methods in the Path
Interface
方法 | 描述 |
---|---|
Path getRoot() |
返回一个代表给定路径根的Path 对象,如果路径没有根,则返回 null。 |
Path getFileName() |
返回给定路径的文件名或目录名。请注意,文件/目录名是给定路径中的最后一个元素或名称。 |
Path getParent() |
返回代表给定路径的父对象的Path 对象,如果该路径不存在父组件,则返回 null。 |
int getNameCount() |
返回给定路径中文件/目录名的数量;如果给定路径代表根,则返回 0。 |
Path getName(int index) |
返回第 I 个文件/目录名;索引 0 从离根最近的名称开始。 |
Path subpath(int beginIndex, int endIndex) |
返回属于此Path 对象的一部分的Path 对象;返回的Path 对象的名字从beginIndex 开始,到索引endIndex - 1 的元素结束。换句话说,beginIndex 包含该索引中的名称,不包含endIndex 中的名称。如果beginIndex 是> =元素个数,或者endIndex <= beginIndex ,或者endIndex >元素个数,这个方法可能会抛出IllegalArgumentException 。 |
Path normalize() |
删除路径中的冗余元素,如. (表示当前目录的点符号)和.. (表示父目录的双点符号)。 |
Path resolve(Path other) Path resolve(String other) |
根据给定路径解析路径。例如,此方法可以将给定路径与另一个路径组合起来,并返回结果路径。 |
Boolean isAbsolute() |
如果给定路径是绝对路径,则返回 true 否则返回 false(例如,当给定路径是相对路径时)。 |
Path startsWith(String path) Path startsWith(Path path) |
如果这个Path 对象以给定的path 开始,则返回 true,否则返回 false。 |
Path toAbsolutePath() |
返回绝对路径。 |
获取路径信息
让我们创建一个Path
对象,并检索与该对象相关的基本信息。清单 10-1 展示了如何创建一个Path
对象并获取关于它的信息。
Listing 10-1. PathInfo1.java
import java.nio.file.Path;
import java.nio.file.Paths;
// Class to illustrate how to use Path interface and its methods
public class PathInfo1 {
public static void main(String[] args) {
// create a Path object by calling static method get() in Paths class
Path testFilePath = Paths.get("D:\\test\\testfile.txt");
// retrieve basic information about path
System.out.println("Printing file information: ");
System.out.println("\t file name: " + testFilePath.getFileName());
System.out.println("\t root of the path: " + testFilePath.getRoot());
System.out.println("\t parent of the target: " + testFilePath.getParent());
// print path elements
System.out.println("Printing elements of the path: ");
for(Path element : testFilePath) {
System.out.println("\t path element: " + element);
}
}
}
该程序打印以下内容:
Printing file information:
file name: testfile.txt
root of the path: D:\
parent of the target: D:\test
Printing elements of the path:
path element: test
path element: testfile.txt
输出是不言自明的。让我们来看看这个程序:
- 首先,使用
Paths
类的get()
方法创建一个Path
实例。get()
方法期望一个代表路径的string
作为输入。这是创建一个Path
对象最简单的方法。 - 请注意,您在
Paths.get("D:\\test\\testfile.txt")
中使用了转义字符(\
)。在路径中,如果你给了D:\test
,那么\t
就意味着一个制表符,当你运行程序时你会得到一个java.nio.file.InvalidPathException
。确保在路径字符串中提供必要的转义字符。 - 使用
Path
对象的getFilename()
方法提取由这个Path
对象表示的文件名。 - 您还可以使用
getRoot()
来获取Path
对象的根元素,使用getParent()
来获取目标文件的父目录。 - 使用一个
for
循环迭代路径中的元素。或者,您可以使用getNameCount()
来获得路径中元素或名称的数量,使用getName(index)
来逐个迭代和访问元素/名称。
让我们试试另一个例子。它探索了一个Path
对象的一些有趣的方面,比如如何从一个相对路径获得一个绝对路径,以及如何规范化一个路径。在看这个例子之前,您需要首先理解它使用的方法:
The
toUri()
方法从路径中返回 URI(可以从浏览器打开的路径)。The
toAbsolutePath()
方法从给定的相对路径中返回绝对路径。如果输入路径已经是绝对路径,则该方法返回相同的对象。normalize()
方法在输入路径上执行标准化。换句话说,它从Path
对象中删除了不必要的符号(如.
和..
)。toRealPath()
是一个有趣的方法。它从输入路径对象返回一个绝对路径(如toAbsolutepath()
)。它也使路径正常化(如在normalize()
)。此外,如果链接选项选择得当,它可以解析符号链接。但是,目标文件/目录必须存在于文件系统中,这不是其他Path
方法的先决条件。
清单 10-2 展示了这个例子。假设文件名Test
在您的文件系统中不存在。
Listing 10-2. PathInfo2.java
import java.io.IOException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
// To illustrate important methods such as normalize(), toAbsolutePath(), and toRealPath()
class PathInfo2 {
public static void main(String[] args) throws IOException {
// get a path object with relative path
Path testFilePath = Paths.get(".\\Test");
System.out.println("The file name is: " + testFilePath.getFileName());
System.out.println("Its URI is: " + testFilePath.toUri());
System.out.println("Its absolute path is: " + testFilePath.toAbsolutePath());
System.out.println("Its normalized path is: " + testFilePath.normalize());
// get another path object with normalized relative path
Path testPathNormalized = Paths.get(testFilePath.normalize().toString());
System.out.println("Its normalized absolute path is: " +
testPathNormalized.toAbsolutePath());
System.out.println("Its normalized real path is: " +
testFilePath.toRealPath (LinkOption.NOFOLLOW_LINKS));
}
}
在我们的机器上,该代码打印了以下内容:
The file name is: Test
Its URI is: file:///D:/OCPJP/programs/NIO2/./Test
Its absolute path is: D:\OCPJP\programs\NIO2\.\Test
Its normalized path is: Test
Its normalized absolute path is: D:\OCPJP\programs\NIO2\Test
Exception in thread "main" java.nio.file.NoSuchFileException: D:\OCPJP\programs\NIO2\Test
at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:79)
[... stack trace elided ...]
at PathInfo2.main(PathInfo2.java:16)
根据运行该程序的目录,目录路径会有所不同。这个程序使用相对路径实例化一个Path
对象。方法getFileName()
返回目标文件名,正如您在上一个例子中看到的。getUri()
方法返回可用于浏览器的 URI,toAbsolutePath()
方法返回给定相对路径的绝对路径。(注意,我们正在执行来自D:/OCPJP/programs/NIO2/
文件夹的程序;因此,它成为当前的工作目录,并出现在绝对路径和 URI。)
您调用normalize()
方法来删除路径中的冗余符号,因此它删除了前导点。(在很多操作系统中,.
【单点符号】代表当前目录,..
【双点】代表父目录。)然后使用规范化输出实例化另一个Path
对象,并再次打印绝对路径。最后,您尝试调用toRealpath();
,但是,您得到一个异常(NoSuchFileException
)。为什么呢?因为,你还没有在当前工作目录下创建Test
目录。
现在,让我们在D:/OCPJP/programs/NIO2/
目录中创建一个Test
目录,并再次运行这个例子。我们得到了以下输出:
The file name is: Test
Its URI is: file:///D:/OCPJP/programs/NIO2/./Test/
Its absolute path is: D:\OCPJP\programs\NIO2\.\Test
Its normalized path is: Test
Its normalized absolute path is: D:\OCPJP\programs\NIO2\Test
Its normalized real path is: D:\OCPJP\programs\NIO2\Test
现在对toRealPath()
的最后一次调用工作正常,并返回绝对规范化路径。
Path
提供了许多其他有用的方法,包括之前在表 10-1 中列出的那些方法。例如,下面是如何使用resolve()
方法:
Path dirName = Paths.get("D:\\OCPJP\\programs\\NIO2\\");
Path resolvedPath = dirName.resolve("Test");
System.out.println(resolvedPath);
这段代码显示了以下内容:
D:\OCPJP\programs\NIO2\Test
这个resolve()
方法认为给定的路径是一个目录,并将传递的路径与其连接(解析),如下所示。
java.io.File
类中的toPath()
方法返回Path
对象;这个方法是在 Java 7 中添加的。同样,您可以使用Path
接口中的toFile()
方法来获得一个File
对象。
比较两条路径
Path
接口提供了两种方法来比较两个Path
对象:equals()
和compareTo()
。equals()
方法检查two Path
对象的相等性并返回一个Boolean
值,而compareTo()
逐字符比较两个Path
对象并返回一个整数:0,如果两个Path
对象相等;如果此路径在字典上小于参数路径,则为负整数;如果该路径在字典上大于参数路径,则为正整数。清单 10-3 包含了一个演示这些方法的小程序。
Listing 10-3. PathCompare1.java
import java.nio.file.Path;
import java.nio.file.Paths;
// illustrates how to use compareTo and equals and also shows
// the difference between the two methods
class PathCompare1 {
public static void main(String[] args) {
Path path1 = Paths.get("Test");
Path path2 = Paths.get("D:\\OCPJP\\programs\\NIO2\\Test");
// comparing two paths using compareTo() method
System.out.println("(path1.compareTo(path2) == 0) is: "
+ (path1.compareTo(path2) == 0));
// comparing two paths using equals() method
System.out.println("path1.equals(path2) is: " + path1.equals(path2));
// comparing two paths using equals() method with absolute path
System.out.println("path2.equals(path1.toAbsolutePath()) is "
+ path2.equals(path1.toAbsolutePath()));
}
}
有意地,一个路径是相对路径,另一个是绝对路径。假设您执行这个程序的当前目录是D:\\OCPJP\\programs\\NIO2\\Test
。你能猜出程序的输出吗?
内容如下:
(path1.compareTo(path2) == 0) is: false
path1.equals(path2) is: false
path2.equals(path1.toAbsolutePath()) is true
让我们一步一步地检查这个程序:
- 它首先使用
compareTo()
方法比较两个路径,逐字符比较路径并返回一个整数。在这种情况下,因为一个路径是相对路径,另一个是绝对路径,所以您首先会得到一条消息,指出这两个路径不相等。 - 然后使用
equals()
比较两条路径。结果是一样的,这意味着即使两个Path
对象指向同一个文件/目录,equals()
也有可能返回 false。您需要确保两个路径都是绝对路径。 - 在下一步中,您将相对路径转换为绝对路径,然后使用
equals()
比较它们。这一次两条路径都匹配。
即使两个Path
对象指向同一个文件/目录,也不能保证equals()
方法会返回 true。您需要确保两者都是绝对的和规范化的路径,以便路径的相等比较成功。
使用文件类
| 认证目标 | 使用 Files 类来检查、读取、删除、复制、移动和管理文件或目录的元数据 |
上一节讨论了如何创建一个Path
实例并从中提取有用的信息。在本节中,您将使用Path
对象来操作文件/目录。Java 7 提供了一个Files
类(在java.nio.file
包中),可以用来对文件或目录执行各种与文件相关的操作。注意Files
是一个实用类,这意味着它是一个带有私有构造函数的最终类,并且只包含静态方法。所以你可以通过调用它提供的静态方法来使用Files
类,比如copy()
来复制文件。这个类提供了广泛的功能。您可以创建目录、文件或符号链接。创建流,如目录流、字节通道和输入/输出流;检查文件的属性;遍历文件树;并执行文件操作,如读、写、复制和删除。表 10-2 提供了Files
类中重要方法的示例。
表 10-2。
Some Important Methods in the Files
Class
方法 | 描述 |
---|---|
Path createDirectory(Path dirPath, FileAttribute<?>... dirAttrs) Path createDirectories(Path dir, FileAttribute<?>... attrs) |
创建一个由dirPath 给出的文件,并设置由dirAttributes 给出的属性。可能会抛出一个异常,比如FileAlreadyExistsException 或者UnsupportedOperationException (比如文件属性不能按照dirAttrs 给的那样设置)。createDirectory 和createDirectories 的区别在于createDirectories 会创建由dirPath 给出的中间目录,如果它们还不存在的话。 |
Path createTempFile(Path dir, String prefix, String suffix, FileAttribute<?>... attrs) |
在由dir 给出的目录中用给定的前缀、后缀和属性创建一个临时文件。 |
Path createTempDirectory(Path dir, String prefix, FileAttribute<?>... attrs) |
在由dir 指定的路径中用给定的前缀和目录属性创建一个临时目录。 |
Path copy(Path source, Path target, CopyOption... options) |
将文件从源复制到目标。CopyOption 可以是 REPLACE_EXISTING,COPY_ATTRIBUTES,或者 NOFOLLOW_LINKS。可以抛出FileAlreadyExistsException 之类的异常。 |
Path move(Path source, Path target, CopyOption... options) |
类似于copy 操作,但是源文件被删除。如果源和目标在同一个目录中,这是一个文件重命名操作。 |
boolean isSameFile(Path path, Path path2) |
检查两个Path 对象是否定位同一个文件。 |
boolean exists(Path path, LinkOption... options) |
检查给定路径中是否存在文件/目录;可以指定LinkOption.NOFOLLOW_LINKS 不跟随符号链接。 |
Boolean isRegularFile(Path path, LinkOption...) |
如果由path 表示的文件是常规文件,则返回 true。 |
Boolean isSymbolicLink(Path path) |
如果由path 表示的文件是一个符号链接,则返回 true。 |
Boolean isHidden(Path path) |
如果由path 表示的文件是隐藏文件,则返回 true。 |
long size(Path path) |
返回由path 表示的文件的大小,以字节为单位。 |
UserPrincipal getOwner(Path path, LinkOption...), Path setOwner(Path path, UserPrincipal owner) |
获取/设置文件的所有者。 |
FileTime getLastModifiedTime(Path path, LinkOption...), Path setLastModifiedTime(Path path, FileTime time) |
获取/设置指定文件的上次修改时间。 |
Object getAttribute(Path path, String attribute, LinkOption...), Path setAttribute(Path path, String attribute, Object value, LinkOption...) |
获取/设置指定文件的指定属性。 |
检查文件属性和元数据
在前面关于Path
接口的部分中,您试图判断两个路径是否指向同一个文件(参见清单 10-3 )。有另一种方法可以发现同样的事情:您可以使用来自Files
类的isSameFile()
方法。清单 10-4 展示了如何做到这一点。
Listing 10-4. PathCompare2.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
// illustrates how to use Files class to compare two paths
class PathCompare2 {
public static void main(String[] args) throws IOException {
Path path1 = Paths.get("Test");
Path path2 = Paths.get("D:\\OCPJP\\programs\\NIO2\\Test");
System.out.println("Files.isSameFile(path1, path2) is: "
+ Files.isSameFile(path1, path2));
}
}
假设您的计算机上存在目录 D:\ \ OCPJP \ \程序\ \ NIO2 \ \测试。该程序打印以下内容:
Files.isSameFile(path1, path2) is: true
在这种情况下,路径D:\OCPJP\programs\NIO2\
中有Test
目录,所以代码工作正常。
如果给定的路径中不存在Test
文件/目录,就会得到一个NoSuchFileException
。但是,如何判断给定路径上是否存在文件/目录呢?Files
类提供了exists()
方法来做到这一点。您还可以使用来自Files
类的isDirectory()
方法来区分文件和目录。清单 10-5 使用了这些方法。
Listing 10-5. PathExists.java
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
class PathExists {
public static void main(String[] args) {
Path path = Paths.get(args[0]);
if(Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
System.out.println("The file/directory " + path.getFileName() + " exists");
// check whether it is a file or a directory
if(Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
System.out.println(path.getFileName() + " is a directory");
}
else {
System.out.println(path.getFileName() + " is a file");
}
}
else {
System.out.println("The file/directory " + path.getFileName()
+ " does not exist");
}
}
}
这个程序从命令行接受一个文件/目录名,并创建一个Path
对象。然后,使用来自Files
类的exists()
方法来找出文件/目录是否存在。exists()
方法的第二个参数是 link option,用于指定是否跟随符号链接;在这种情况下,您没有跟踪符号链接。如果与输入路径相关联的文件/目录存在,那么使用Files
类的isDirectory()
方法检查输入路径指示的是文件还是目录。
我们用两个不同的命令行参数运行这个程序,得到了下面的输出(假设PathExists.java
存储在目录D:\OCPJP\programs\NIO2\src
中):
D:\OCPJP\programs\NIO2\src>java PathExists PathExists.java
The file/directory PathExists.java exists
PathExists.java is a file
D:\OCPJP\programs\NIO2\src>java PathExists D:\OCPJP\
The file/directory OCPJP exists
OCPJP is a directory
D:\OCPJP\programs\NIO2\src>java PathExists D:\
The file/directory null exists
null is a directory
在这个输出中,您可能已经注意到了根目录名(在本例中是 Windows 中的驱动器名)作为参数给出时的行为。根目录名是一个目录,但是如果路径是根目录名的话,path.getFileName()
返回 null 这就是结果。
基于您的凭据,现有文件可能不允许您读取、写入或执行。您可以检查程序以编程方式读取、写入或执行的能力。Files
类提供了方法isReadable()
、isWritable()
和isExecutable()
来实现这一点。清单 10-6 使用了这些方法:为这个程序创建一个名为readonly.txt
的文件,其权限是可读可执行但不可写。
Listing 10-6. FilePermissions.java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
class FilePermissions {
public static void main(String[] args) {
Path path = Paths.get(args[0]);
System.out.printf( "Readable: %b, Writable: %b, Executable: %b ",
Files.isReadable(path), Files.isWritable(path), Files.isExecutable(path));
}
}
让我们用两个不同的输入来执行这个程序。以下是输出:
D:\OCPJP\programs\NIO2\src>java FilePermissions readonly.txt
Readable: true, Writable: false, Executable: true
D:\OCPJP\programs\NIO2\src>java FilePermissions FilePermissions.java
Readable: true, Writable: true, Executable: true
对于readonly.txt
文件,权限是可读和可执行的,但不可写。文件FilePermissions.java
拥有所有三种权限:可读、可写和可执行。
您可以使用许多其他方法来获取文件属性。让我们使用getAttribute()
方法来获取文件的一些属性。该方法接受可变数量的参数:一个Path
对象、一个属性名和链接选项(参见清单 10-7 )。
Listing 10-7. FileAttributes.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
class FileAttributes {
public static void main(String[] args) {
Path path = Paths.get(args[0]);
try {
Object object = Files.getAttribute(path, "creationTime",
LinkOption.NOFOLLOW_LINKS);
System.out.println("Creation time: " + object);
object = Files.getAttribute(path, "lastModifiedTime", LinkOption.NOFOLLOW_LINKS);
System.out.println("Last modified time: " + object);
object = Files.getAttribute(path, "size", LinkOption.NOFOLLOW_LINKS);
System.out.println("Size: " + object);
object = Files.getAttribute(path, "dos:hidden", LinkOption.NOFOLLOW_LINKS);
System.out.println("isHidden: " + object);
object = Files.getAttribute(path, "isDirectory", LinkOption.NOFOLLOW_LINKS);
System.out.println("isDirectory: " + object);
} catch (IOException e) {
e.printStackTrace();
}
}
}
让我们首先通过给出这个程序的名字来执行这个程序,然后看看会发生什么:
D:\> java FileAttributes FileAttributes.java
Creation time: 2012-10-06T10:20:10.34375Z
Last modified time: 2012-10-06T10:21:54.859375Z
Size: 914
isHidden: false
isDirectory: false
这个例子中棘手的部分是getAttribute()
方法的第二个参数。您需要提供正确的属性名来提取相关的值。应该以view:attribute
格式指定期望的字符串,其中view
是FileAttributeView
的类型,attribute
是view
支持的属性的名称。如果没有指定view
,则假定为basic
。在这种情况下,您指定属于一个基本视图的所有属性,除了来自dos
视图的一个属性。如果没有指定正确的视图名,就会得到一个UnsupportedOperationException
;如果你弄乱了属性名,你会得到一个IllegalArgumentException
。
例如,如果您键入 size 而不是 size,您将得到以下异常:
Exception in thread "main" java.lang.IllegalArgumentException: 'sized' not recognized
[...stack trace elided...]
现在,您知道了如何使用getAttribute()
方法读取与文件相关的元数据。然而,如果您想要读取许多属性,为每个属性调用getAttribute()
可能不是一个好主意(从性能的角度来看)。在这种情况下,Java 7 提供了一个解决方案:一个 API——readAttributes()
——一次性读取属性。API 有两种风格:
Map<String,Object> readAttributes(Path path, String attributes, LinkOption... options)
<A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
第一个方法返回一个属性-值对的Map
,并接受可变长度的参数。attributes
参数是关键参数,您可以在其中指定想要检索的内容。该参数类似于您在getAttribute()
方法中使用的参数;但是,这里可以指定一个属性列表,也可以使用星号(*
)来指定所有属性。例如,使用*
表示默认FileAttributeView
的所有属性,如BasicFileAttributes
(指定为基本档案属性)。再比如dos:*
,指 dos 文件属性的所有属性。
第二种方法使用通用语法(第四章)。这里的第二个参数从BasicFileAttributes
层次结构中获取一个类,稍后将对此进行讨论。该方法从BasicFileAttributes
层次结构中返回一个实例。
文件属性层次结构如图 10-1 所示。BasicFileAttributes
是派生DosFileAttributes
和PosixFileAttributes
的基础接口。注意这些属性接口是在java.nio.file.attribute
包中提供的。
图 10-1。
The hierarchy of BasicFileAttributes
如你所见,BasicFileAttributes
接口定义了所有通用平台支持的基本属性。但是,特定的平台定义了自己的文件属性,这些属性由DosFileAttributes
和PosixFileAttributes
捕获。您可以指定这些接口中的任何一个来检索相关的文件属性。清单 10-8 包含一个使用BasicFileAttributes
获取文件所有属性的程序。
Listing 10-8. FileAttributes2.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
class FileAttributes2 {
public static void main(String[] args) {
Path path = Paths.get(args[0]);
try {
BasicFileAttributes fileAttributes =
Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("File size: " + fileAttributes.size());
System.out.println("isDirectory: " + fileAttributes.isDirectory());
System.out.println("isRegularFile: " + fileAttributes.isRegularFile());
System.out.println("isSymbolicLink: " + fileAttributes.isSymbolicLink());
System.out.println("File last accessed time: " + fileAttributes.lastAccessTime());
System.out.println("File last modified time: " +
fileAttributes.lastModifiedTime());
System.out.println("File creation time: " + fileAttributes.creationTime());
} catch (IOException e) {
e.printStackTrace();
}
}
}
以下是该程序的一些示例输出:
D:\>java FileAttributes2 FileAttributes2.java
File size: 904
isDirectory: false
isRegularFile: true
isSymbolicLink: false
File last accessed time: 2012-10-06T10:28:29.0625Z
File last modified time: 2012-10-06T10:28:22.4375Z
File creation time: 2012-10-06T10:26:39.1875Z
您使用readAttribute()
方法和BasicFileAttributes
来检索基本的文件属性。类似地,您可以分别使用DosFileAttributes
和PosixFileAttributes
在 DOS 或 Unix 环境中检索与文件相关的属性。
复制文件
现在让我们试着将一个文件/目录从一个位置复制到另一个位置。这个任务很容易完成:只需调用Files.copy()
将文件从源复制到目标。下面是这个方法的签名:
Path copy(Path source, Path target, CopyOption... options)
清单 10-9 使用这种方法编写了一个简单的文件复制程序。
Listing 10-9. FileCopy.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileCopy {
public static void main(String[] args) {
if(args.length != 2){
System.out.println("usage: FileCopy <source-path> <destination-path>");
System.exit(1);
}
Path pathSource = Paths.get(args[0]);
Path pathDestination = Paths.get(args[1]);
try {
Files.copy(pathSource, pathDestination);
System.out.println("Source file copied successfully");
} catch (IOException e) {
e.printStackTrace();
}
}
}
让我们执行它,看看它是否有效。
D:\> java FileCopy FileCopy.java Backup.java
Source file copied successfully
是的,起作用了。尝试使用相同的参数再次运行它:
D:\OCPJP\programs\NIO2\src>java FileCopy FileCopy.java Backup.java
java.nio.file.FileAlreadyExistsException: Backup.java
at sun.nio.fs.WindowsFileCopy.copy(Unknown Source)
[...stack trace elided...]
哎呀!发生了什么事?当您第二次尝试复制文件时,您会得到一个FileAlreadyExistsException
,因为目标文件已经存在。如果你想覆盖现有的文件呢?解决方案:您需要告诉copy()
方法您想要覆盖一个现有的文件。在清单 10-9 中,将copy()
修改如下:
Files.copy(pathSource, pathDestination, StandardCopyOption.REPLACE_EXISTING);
您指定一个额外的参数(因为copy()
方法支持可变参数)来告诉该方法您想要覆盖一个已经存在的文件。运行这个程序,看看它是否工作:
D:\>java FileCopy FileCopy.java Backup.java
Source file copied successfully
D:\>java FileCopy FileCopy.java Backup.java
Source file copied successfully
是的,它有效。现在,尝试将文件复制到新目录:
D:\OCPJP\programs\NIO2\src>java FileCopy FileCopy.java bak\Backup.java
java.nio.file.NoSuchFileException: FileCopy.java -> bak\Backup.java
[...stack trace elided ...]
这里,您试图将一个文件复制回一个不存在的目录。所以,你得到了NoSuchFileException
。不仅仅是给定的目录,路径上的所有中间目录都必须存在,这样copy()
方法才能成功。
指定路径上的所有目录(除了最后一个,如果你正在复制一个目录)必须存在,以避免
NoSuchFileException
。
如果你试着复制一个目录呢?它可以工作,但是记住它只会复制顶层目录,而不会复制该目录中包含的文件/目录。
如果使用
copy()
方法复制一个目录,它不会复制源目录中包含的文件/目录;您需要显式地将它们复制到目标文件夹。
移动文件
移动文件类似于复制文件;为此,您可以使用Files.move()
方法。此方法的签名如下:
Path move(Path source, Path target, CopyOption... options)
清单 10-10 包含了一个使用这种方法的小程序。注意,一旦move()
方法成功执行,源文件就不再存在。
Listing 10-10. FileMove.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
public class FileMove {
public static void main(String[] args) {
if(args.length != 2){
System.out.println("usage: FileMove <source-path> <destination-path>");
System.exit(-1);
}
Path pathSource = Paths.get(args[0]);
Path pathDestination = Paths.get(args[1]);
try {
Files.move(pathSource, pathDestination, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Source file moved successfully");
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个程序是这样执行的(假设当前目录中存在一个名为text.txt
的文件):
D:\OCPJP\programs\NIO2\src> java FileMove text.txt newtext.txt
Source file moved successfully
以下是对move()
方法的一些观察:
- 像
copy()
方法一样,move()
方法不会覆盖现有的目标文件,除非您使用REPLACE_EXISTING
指定它应该这样做。 - 如果移动符号链接,则移动的是链接本身,而不是链接的目标文件。需要注意的是,在
copy()
的情况下,如果你指定一个符号链接,链接的目标被复制,而不是链接本身。 - 如果移动目录不需要移动包含的文件/目录,则可以移动非空目录。例如,将一个目录从一个物理驱动器移动到另一个可能不成功(将抛出一个
IOException
)。如果移动目录成功,那么所有包含的文件/目录也会被移动。 - 您可以使用
ATOMIC_MOVE
复制选项将move()
指定为原子操作。当您指定原子移动时,可以确保移动成功完成或源继续存在。如果move()
作为非原子操作执行,并且在执行过程中失败,那么两个文件的状态都是未知和未定义的。
删除文件
Files
类提供了一个delete()
方法来删除一个文件/目录/符号链接。清单 10-11 包含一个删除指定文件的简单程序。
Listing 10-11. FileDelete.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileDelete {
public static void main(String[] args) {
if(args.length != 1){
System.out.println("usage: FileDelete <source-path>");
System.exit(1);
}
Path pathSource = Paths.get(args[0]);
try {
Files.delete(pathSource);
System.out.println("File deleted successfully");
} catch (IOException e) {
e.printStackTrace();
}
}
}
它在执行时打印以下内容:
D:\> java FileDelete log.txt
File deleted successfully
使用Files.delete()
方法时有几点需要记住。在目录的情况下,应该在空目录上调用delete()
方法;否则,该方法将失败。在符号链接的情况下,删除的是链接,而不是链接的目标文件。您要删除的文件必须存在;否则你会得到一个NoSuchFileException
。如果您静默删除一个文件,并且不想被这个异常所困扰,那么您可以使用deleteIfExists()
方法,如果文件不存在,它不会报错,如果文件存在,它会删除文件。此外,如果文件是只读的,某些平台可能会阻止您删除该文件。
要记住的要点
记住这些要点,帮助你通过 OCPJP 八级考试。
- 不要混淆
File
和Files
,以及Paths
的路径:它们是不同的。File
是一个旧类(Java 4 ),表示文件/目录路径名,而Files
是 Java 7 中引入的一个实用类,对 I/O API 有全面的支持。Path
接口表示一个文件/目录路径,并定义了一个有用的方法列表。然而,Paths
类是一个只提供两种方法的实用程序类(都是为了获取Path
对象)。 - 由
Path
对象表示的文件或目录可能不存在。除了像toRealPath()
这样的方法之外,Path
中的方法不需要为Path
对象提供底层文件或目录。 - 您学习了如何对文件/目录执行复制。但是,没有必要只对两个文件/目录执行复制。您可以从一个
InputStream
获取输入并写入一个文件,或者您可以从一个文件获取输入并复制到一个OutputStream
。这里可以使用copy(InputStream, Path, CopyOptions...)
和copy(Path, OutputStream, CopyOptions...)
两种方法。
在 NIO.2 中使用流 API
| 认证目标 | 将流 API 与 NIO.2 一起使用 |
Java 8 中对 JDK 的大量增强简化了使用 NIO.2 的编程。本节讨论 Java 8 中对java.nio
包的一些重要增强。
在 Files 类中使用 list()方法
让我们首先使用 Java 8 中添加的Files.list()
方法来列出当前目录中的所有文件(参见清单 10-12 )。在底层,它使用了一个DirectoryStream
,因此必须调用close()
方法来释放 I/O 资源。这个程序使用带有自动关闭流的try-with-resources
语句的流。
Listing 10-12. ListFiles.java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;
class ListFiles {
public static void main(String []args) throws IOException {
try(Stream<Path> entries = Files.``list``(Paths.``get
entries.forEach(System.``out
}
}
}
它打印了当前目录中的文件:
./ListFiles.class
./ListFiles.java
... (rest of the output elided)
list()
方法声明如下:
static Stream<Path> list(Path dir) throws IOException
因为list()
方法返回一个Stream
,所以您可以使用Stream
接口中提供的众多方法中的任何一种,包括map()
、filter()
、findFirst()
、findAny()
、distinct()
、sorted()
、allMatch()
、noneMatch()
和anyMatch()
。
这段代码是清单 10-12 的修改版本,它打印文件的绝对路径:
Files.list(Paths.get("."))
.map(path -> path.toAbsolutePath())
.forEach(System.out::println);
结果如下:
D:\OCPJP\NIO2\src\ListFiles.class
D:\OCPJP\NIO2\src\ListFiles.java
... (rest of the output elided)
注意,list()
方法不会递归地遍历给定的Path
中的条目。要递归地遍历目录,可以使用Files.walk()
方法:
Files.walk(Paths.get(".")).forEach(System.out::println);
Files.walk()
方法是一个重载方法:
static Stream<Path> walk(Path path, FileVisitOption... options) throws IOException
static Stream<Path> walk(Path path, int maxDepth, FileVisitOption... options) throws IOException
FileVisitOption
有一个枚举值:FileVisitOption.FOLLOW_LINKS
。你可以把它传递给walk()
方法。您还可以指定maxDepth
:递归遍历目录条目的嵌套层次的限制(参见清单 10-13 )。
Listing 10-13. CountEntriesRecur.java
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;
class CountEntriesRecur {
public static void main(String []args) throws IOException {
try(Stream<Path> entries =
Files.walk(Paths.get("."), 4, FileVisitOption.FOLLOW_LINKS)) {
long numOfEntries = entries.count();
System.out.printf("Found %d entries in the current path", numOfEntries);
}
}
}
在我们的机器上,这个程序打印了以下内容:
Found 179 entries in the current path
这段代码将嵌套深度限制为 4,作为Files.walk()
方法的第二个参数。
最后,让我们使用Files.
find()
方法列出符合给定条件的文件(列出 10-14 )。
Listing 10-14. FindFiles.java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.function.BiPredicate;
import java.util.stream.Stream;
class FindFiles {
public static void main(String []args) throws IOException {
BiPredicate<Path, BasicFileAttributes> predicate = (path, attrs)
-> attrs.isRegularFile() && path.toString().endsWith("class");
try(Stream<Path> entries = Files.find(Paths.get("."), 4, predicate)) {
entries.limit(100).forEach(System.out::println);
}
}
}
这个程序打印长输出,所以这里不给出。
这个例子在Stream<Path>
对象上使用了limit()
方法来限制从Files.find()
方法返回时处理的条目数量。find()
方法将开始搜索的路径、要搜索的最大深度、BiPredicate
和可选的FileVisitOption
作为参数:
static Stream<Path> find(Path path, int maxDepth, BiPredicate<Path,BasicFileAttributes>
matcher, FileVisitOption... options) throws IOException
在这个例子中,您正在寻找以扩展名class
结尾的文件,并且您将条目的数量限制为 100。
使用 Files 类中的 lines()方法
Files.lines()
是一种非常方便的读取文件内容的方法:
static Stream<String> lines(Path path)
在内部,它使用一个Reader
,因此在使用后必须关闭。您使用清单 10-15 中的try-with-resources
来打印文件名作为参数传递的文件的内容。
Listing 10-15. Type.java
import java.io.IOException;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.stream.Stream;
// implements a simplified version of "type" command provided in Windows;
// given a text file name(s) as argument, it prints the content of the file(s)
class Type {
private static void processFile(String file) {
try(Stream<String> lines = Files.``lines``(Paths.``get
lines.forEach(System.``out
} catch (IOException ioe) {
System.``err
System.``exit
}
}
public static void main(String[] files) throws IOException {
if (files.length == 0) {
System.``err
System.``exit
}
// process each file passed as argument
Arrays.``stream``(files).forEach(Type::``processFile
}
}
这段代码比你在 IO 基础章节中看到的版本要简洁得多(在第九章中列出了 9-5 )。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试前阅读这一部分。
使用路径接口操作文件和目录路径
- 一个
Path
对象是一个表示文件/目录路径的编程抽象。 - 您可以使用
Paths
类的get()
方法获得Path
的实例。 Path
提供了两种比较Path
对象的方法:equals()
和compareTo()
。即使两个Path
对象指向同一个文件/目录,也不能保证equals()
方法返回 true。
使用 Files 类来检查、读取、删除、复制、移动和管理文件或目录的元数据
- 您可以使用
Files
类的exists()
方法来检查文件是否存在。 Files
类提供了方法isReadable()
、isWritable()
和isExecutable()
来分别检查程序以编程方式读取、写入和执行的能力。- 您可以使用
getAttributes()
方法检索文件的属性。 - 您可以使用
Files
类的readAttributes()
方法来批量读取文件属性。 copy()
方法可用于将文件从一个位置复制到另一个位置。类似地,move()
方法将文件从一个位置移动到另一个位置。- 复制时,指定路径上的所有目录(除了最后一个,如果您正在复制一个目录)必须存在,以避免出现
NoSuchFileException
。 - 使用
delete()
方法删除文件;使用deleteIfExists()
方法删除一个存在的文件。
将流 API 与 NIO.2 一起使用
Files.list()
方法返回一个Stream<Path>
。它不会递归地遍历给定的Path
中的目录。Files.walk()
方法通过从给定的Path;
的一个重载版本中递归遍历条目来返回一个Stream<Path>
,您也可以传递这种遍历的最大深度,并提供FileVisitOption.FOLLOW_LINKS
作为第三个选项。Files.find()
方法通过递归遍历来自给定Path;
的条目返回一个Stream<Path>
,它还将最大搜索深度、一个BiPredicate
和一个可选的FileVisitOption
作为参数。Files.lines()
是一种非常方便的读取文件内容的方法。它返回一个Stream<String>
。
Question TimeConsider the following program: import java.nio.file.*;
public class PathInfo {
public static void main(String[] args) {
Path aFilePath = Paths.get("D:\\directory\\file.txt"); // FILEPATH
while(aFilePath.iterator().hasNext()) {
System.out.println("path element: " + aFilePath.iterator().next());
}
}
}
Assume that the file D:\directory\file.txt
exists in the underlying file system. Which one of the following options correctly describes the behavior of this program? The program gives a compiler error in the line marked with the comment FILEPATH
because the checked exception FileNotFoundException
is not handled. The program gives a compiler error in the line marked with the comment FILEPATH
because the checked exception InvalidPathException
is not handled. The program gets into an infinite loop, printing “path element: directory” forever. The program prints the following: path element: directory
path element: file.txt
Consider the following program: import java.nio.file.*;
class SubPath {
public static void main(String []args) {
Path aPath = Paths.get("D:\\OCPJP\\programs\\..\\NIO2\\src\\.\\SubPath.java");
aPath = aPath.normalize();
System.out.println(aPath.subpath(2, 3));
}
}
This program prints the following: ..
src
NIO2
NIO2\src
..\NIO2
Consider the following program: import java.nio.file.*;
import java.io.IOException;
class PathExists {
public static void main(String []args) throws IOException {
Path aFilePath = Paths.get("D:\\directory\\file.txt");
System.out.println(aFilePath.isAbsolute());
}
}
Assuming that the file D:\directory\file.txt does not exist, what will be the behavior of this program? This program prints false
. This program prints true
. This program crashes by throwing a java.io.IOException
. This program crashes by throwing a java.nio.file.NoSuchFileException
. Given this code segment (assume that necessary import
statements are provided in the program that contains this code segment) Stream<String> lines = Files.
lines
(Paths.
get
("./text.txt"))
// line n1
If a file named text.txt exists in the current directory in which you are running this code segment, which one of the following statements will result in printing the first line of the file’s contents? lines.limit(1).forEach(System.out::println);
lines.forEach(System.out::println);
lines.println();
lines.limit(1).println();
lines.forEach(1);
Consider the following code segment: try(Stream<Path> entries = Files.find(Paths.get("."), 4, predicate)) {
entries.forEach(System.out::println);
}
Which one of the following is a valid definition of the variable predicate that can be used in this code segment? BiPredicate<Path, BasicFileAttributes> predicate = (path, attrs) -> true;
Predicate<Path> predicate = (path) -> true
Predicate<BasicFileAttributes> predicate = (attrs) -> attrs.isRegularFile();
Predicate predicate = FileVisitOption.FOLLOW_LINKS;
答案:
C. The program gets into an infinite loop, printing “path element: directory” forever. In the while
loop, you use iterator()
to get a temporary iterator object. So, the call to next()
on the temporary variable is lost, and the while
loop gets into an infinite loop. In other words, the following loop terminates after printing the directory and file.txt
parts of the path: Iterator<Path> paths = aFilePath.iterator();
while(paths.hasNext()) {
System.out.println("path element: " + paths.next());
}
Option A is wrong because the Paths.get
method does not throw FileNotFoundException
. Option B is wrong because InvalidPathException
is a RuntimeException
. Also, even if the file path does not exist in the underlying file system, this exception will not be thrown when the program is executed. Option D is wrong because the program gets into an infinite loop. B. src The normalize()
method removes redundant name elements in the given path, so after the call to the normalize()
method, the aPath
value is D:\OCPJP\NIO2\src\SubPath.java
. The subpath(int beginIndex, int endIndex)
method returns a path based on the values of beginIndex
and endIndex
. The name that is closest to the root has index 0; note that the root itself (in this case, D:\
) is not considered an element in the path. Hence, the name elements “OCPJP”, “NIO2”, “src”, and “SubPath.java” are in index positions 0, 1, 2, and 3, respectively. Note that beginIndex
is the index of the first element, inclusive of that element; endIndex
is the index of the last element, exclusive of that element. Hence, the subpath is src
, which is at index position 2 in this path. B. This program prints: true To use methods such as isAbsolute()
, the actual file need not exist. Because the path represents an absolute path (and not a relative path), this program prints true. A. lines.limit(1).forEach(System.out::println);
The limit(1)
method truncates the result to one line; and the forEach()
method, when passed with the System.out::println
method reference, prints that line to the console. Option B prints all the lines in the given file and thus is the wrong answer. The code segments given in the other three options will result in compiler errors. A. BiPredicate<Path, BasicFileAttributes> predicate = (path, attrs) -> true;
The find()
method takes the path to start searching from, the maximum depth to search, a BiPredicate
, and an optional FileVisitOption
as arguments: static Stream<Path> find(Path path, int maxDepth, BiPredicate<Path,BasicFileAttributes>
matcher, FileVisitOption... options) throws IOException
Option A provides a definition of BiPredicate
and hence it is the correct answer. Using the other options will result in a compiler error.
十一、Java 并发
认证目标 |
---|
使用 Runnable、Callable 创建工作线程,并使用 ExecutorService 并发执行任务 |
识别死锁、饥饿、活锁和竞争条件中潜在的线程问题 |
使用 synchronized 关键字和 java.util.concurrent.atomic 包来控制线程执行的顺序 |
使用 java.util.concurrent 集合和类,包括 CyclicBarrier 和 CopyOnWriteArrayList |
使用并行 Fork/Join 框架 |
使用并行流,包括缩减、分解、合并流程、管道和性能 |
随着多核处理器的广泛使用,并发性变得越来越重要。concurrency 这个词的拉丁语词根是“一起运行”的意思在编程中,可以让多个线程在一个程序中并行运行,同时执行不同的任务。正确使用时,并发可以提高应用的性能和响应能力,因此它是一个强大而有用的特性。在本章中,我们互换使用多线程和并发这两个术语。
从一开始,Java 就以低级线程管理、锁、同步和并发 API 的形式支持并发。从 5.0 开始,Java 也在其java.util.concurrent
包中支持高级并发 API。从 8.0 版本开始,随着并行流的引入,Java 获得了对并发性更好的支持。
OCPJP 8 考试目标涵盖了与并发性相关的广泛主题,包括创建工作线程和使用并行流的不同方式。在你的考试中,你会从这个题目中想到很多问题。为此,我们将在本章详细讨论 Java 的并发支持。
创建线程以并发执行任务
认证目标 | |
---|---|
使用 Runnable、Callable 创建工作线程,并使用 ExecutorService 并发执行任务 |
Thread
和Object
类以及Runnable
接口为 Java 中的并发性提供了必要的支持。Thread
类有对多线程有用的run()
、start()
和sleep()
等方法(表 11-1 列出了Thread
类中的重要方法)。Object
类有像wait()
和notify()
这样支持并发的方法。因为 Java 中的每个类都是从Object
类派生的,所以所有的对象都有一些基本的多线程能力。例如,你可以在 Java 中获取任何对象的锁(使用synchronized
关键字,我们将在本章后面讨论)。然而,要创建一个线程,这个来自Object
的基本支持是没有用的。为此,一个类应该扩展Thread
类或者实现Runnable
接口。Thread
和Runnable
都在java.lang
包中,所以你不必为了写多线程程序而显式导入这些类。
表 11-1。
Important Methods in the Thread
Class
方法 | 方法类型 | 简短描述 |
---|---|---|
Thread currentThread() |
静态法 | 返回对当前线程的引用。 |
String getName() |
实例方法 | 返回当前线程的名称。 |
int getPriority() |
实例方法 | 返回当前线程的优先级值。 |
void join(),``void join(long), |
重写的实例方法 | 调用另一个线程上的 join 的当前线程等待,直到另一个线程完成。您可以选择以毫秒为单位给出超时时间(在long 中给出),或者以毫秒和纳秒为单位给出超时时间(在long 和int 中给出)。 |
void run() |
实例方法 | 一旦你启动了一个线程(使用start() 方法),当线程准备执行时run() 方法将被调用。 |
void setName(String) |
实例方法 | 将线程的名称更改为参数中的给定名称。 |
void setPriority(int) |
实例方法 | 将线程的优先级设置为给定的参数值。 |
void sleep(long) void sleep(long, int) |
过载的静态方法 | 让当前线程休眠给定的毫秒数(在long 中给出)或给定的毫秒数和纳秒数(在long 和int 中给出)。 |
void start() |
实例方法 | 启动线程;JVM 调用线程的run() 方法。 |
String toString() |
实例方法 | 返回线程的字符串表示形式;该字符串包含线程的名称、优先级和组。 |
创建线程
现在让我们使用Thread
类和Runnable
接口创建线程。我们将在本章后面讨论如何使用Callable
和ExecutorService
创建工作线程。
通过扩展Thread
类创建线程
要扩展Thread
类,您需要覆盖run()
方法。如果你不覆盖run()
方法,默认的Thread
类中的run()
方法将被调用,这没有任何作用。要覆盖run()
方法,需要将其声明为public
;它没有参数,有一个void
返回类型;换句话说,应该声明为public void run()
。
您可以通过调用Thread
类的对象上的start()
方法来创建一个线程(清单 11-1 )。当 JVM 调度线程时,它会将线程转移到可运行状态,然后执行run()
方法。当run()
方法完成执行并返回时,线程将终止。
Listing 11-1. MyThread.java
class MyThread extends Thread {
public void run() {
try {
sleep
(1000);
}
catch (InterruptedException ex) {
ex.printStackTrace();
// ignore the InterruptedException - this is perhaps the one of the
// very few of the exceptions in Java which is acceptable to ignore
}
System.``out
}
public static void main(String args[]) {
Thread myThread = new MyThread();
myThread.start();
System.``out
Thread.``currentThread
}
}
该程序打印以下内容:
In main(); thread name is: main
In run(); thread name is: Thread-0
在这个例子中,MyThread
类扩展了Thread
类。你已经覆盖了这个类中的run()
方法。这个run()
方法将在线程运行时被调用。在main()
函数中,您创建了一个新线程,并使用start()
方法启动它。一个重要的注意事项:不要直接调用run()
方法。相反,您使用start()
方法启动线程;JVM 自动调用run()
方法。
要打印线程的名称,可以使用实例方法getName()
,它返回一个String
。因为main()
是一个静态方法,所以你不能访问this
引用。因此,您可以使用Thread
类中的静态方法currentThread()
获得当前线程名称(返回一个Thread
对象)。现在您可以对返回的对象调用getName
。稍后您会看到,main()
方法也是作为一个线程执行的!但是,在run()
方法内部,您可以直接调用getName()
方法:MyThread
扩展了Thread
,因此所有基类成员在MyThread
中也是可用的。
通过实现Runnable
接口创建线程
创建线程的另一种方法是实现Runnable
接口。Thread
类本身实现了Runnable
接口。Runnable
接口声明了一个唯一的方法run()
。因此,当您实现Runnable
接口时,您需要定义run()
方法。记住Runnable
没有声明start()
方法。那么,如果实现了Runnable
接口,如何创建线程呢?Thread
有一个重载的构造函数,它接受一个Runnable
对象作为参数。清单 11-2 实现了Runnable
接口并创建了一个Thread
。
Listing 11-2. RunnableImpl.java
class RunnableImpl implements Runnable {
public void run() {
System.``out
Thread.``currentThread
}
public static void main(String args[]) throws Exception {
Thread myThread = new Thread(new RunnableImpl());
myThread.start();
System.``out
Thread.``currentThread
}
}
该程序打印:
In main(); thread name is: main
In run(); thread name is: Thread-0
你在这个程序中实现了run()
方法。但是,要获得字符串的名称,必须遵循迂回路线,用Thread.currentThread().getName()
获得线程名称。在main()
方法中,要创建一个线程,你必须将RunnableImpl
类的对象传递给Thread
构造函数。start()
方法启动线程,JVM 稍后调用线程的run()
方法。
用synchronized
关键字进行线程同步
认证目标 | |
---|---|
使用 synchronized 关键字和 java.util.concurrent.atomic 包来控制线程执行的顺序 |
Java 的synchronized
关键字有助于线程同步。您可以以两种形式使用它:同步块和同步方法。为什么我们需要使用synchronized
关键字?以避免竞态条件的问题。现在让我们来讨论这个话题。
竞赛条件
线程共享内存,它们可以并发地修改数据。由于修改可以在没有安全措施的情况下同时进行,这可能导致不直观的结果。
当两个或多个线程试图访问一个变量,而其中一个想要修改它时,就会出现一个称为竞争条件的问题(也称为数据竞争或竞争危险)。清单 11-3 显示了一个竞争条件的例子。
Listing 11-3. RaceCondition.java
// This class exposes a publicly accessible counter
// to help demonstrate race condition problem
class Counter {
public static long``count
}
// This class implements Runnable interface
// Its run method increments the counter three times
class UseCounter implements Runnable {
public void increment() {
// increments the counter and prints the value
// of the counter shared between threads
Counter.``count
System.``out``.print(Counter.``count
}
public void run() {
increment();
increment();
increment();
}
}
// This class creates three threads
public class RaceCondition {
public static void main(String args[]) {
UseCounter c = new UseCounter();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
Thread t3 = new Thread(c);
t1.start();
t2.start();
t3.start();
}
}
在这个程序中,有一个包含静态变量count
的Counter
类。在UseCounter
类的run()
方法中,通过调用increment()
方法将count
递增三次。您在RaceCondition
类的main()
函数中创建三个线程并启动它。随着线程的运行和计数器的递增,您希望程序按顺序打印 1 到 9。但是,当您运行这个程序时,它确实打印了 9 个整数值,但是输出看起来像垃圾!在一次示例运行中,我们得到了这些值:
3 3 5 6 3 7 8 4 9
请注意,每次运行该程序时,这些值通常会有所不同;当我们再运行两次时,我们得到了这些输出:
3 3 5 6 3 4 7 8 9
3 3 3 6 7 5 8 4 9
那么,问题是什么呢?
表达式Counter.count++
是一个写操作,下一个System.out.print
语句对Counter.count
有一个读操作。当三个线程执行时,它们中的每一个都有值Counter.count
的本地副本,并且当它们用Counter.count++
更新counter
时,它们不需要立即在主存储器中反映该值(参见图 11-1 )。在Counter.count
的下一次读取操作中,打印Counter.count
的本地值。
图 11-1。
Threads t1, t2, and t3 trying to change Counter.count, causing a race condition
因此,这个程序受到竞争条件问题的困扰。为了避免这个问题,您需要确保单个线程一起执行写和读操作(原子地)。通常被多个线程访问和修改的代码段称为临界段。为了避免竞争条件问题,您需要确保临界区一次只由一个线程执行。
你是怎么做到的?通过使用关键字synchronized
获取对象上的锁,我们现在将讨论这一点。一次只有一个线程可以获得对象上的锁,并且只有该线程可以执行受锁保护的代码块(即临界区)。在那之前,其他线程必须等待。
同步块
在 synchronized 块中,使用关键字synchronized
作为引用变量,并在其后跟随一个代码块。线程必须获得同步变量的锁才能进入该块;当块的执行完成时,线程释放锁。例如,如果代码块在一个非静态方法中,您可以获取一个对this
引用的锁:
synchronized(this) {
// code segment guarded by the mutex lock
}
如果在 synchronized 块中抛出了一个异常怎么办?锁会被打开吗?是的,不管这个块是完全执行还是抛出异常,锁都会被 JVM 自动释放。使用同步块,您只能在引用变量上获得锁。如果使用基本类型,将会出现编译器错误。
让我们通过在increment()
方法中添加一个同步块来解决清单 11-3 中的竞争条件问题,如下所示:
// within the UseCounter class
public void increment() {
// increments the counter and prints the value
// of the counter shared between threads
synchronized(this) {
Counter.count++;
System.out.print(Counter.count + " ");
}
}
随着这一改变,程序打印正确增加的值count
:
1 2 3 4 5 6 7 8 9
在increment()
方法中,在读取和写入Counter.count
之前,您获得了对this
引用的锁定。因此,不可能有多个线程同时执行这些语句。因为只有一个线程可以获得锁并执行“临界区”代码块,所以在给定时间,计数器只增加一个线程;结果,程序正确地打印出值 1 到 9。如果没有synchronized
块,三个不同的线程将自由地修改变量,因此您将无法正确地打印出值 1 到 9(因为我们前面讨论过的竞争条件问题)。
同步方法
一个完整的方法可以声明为synchronized
。在这种情况下,当调用声明为 synchronized 的方法时,会在调用该方法的对象上获得一个锁,当该方法返回到调用方时,该锁会被释放。这里有一个例子:
public synchronized void assign(int i) {
val = i;
}
现在assign()
方法是一个同步的方法。如果您调用assign()
方法,它将隐式获取对this
引用的锁,然后执行语句val = i;
。如果其他线程已经获得了锁,会发生什么呢?就像同步块一样,如果线程不能获得锁,它将被阻塞,线程将等待直到锁可用。
如果您将整个方法体包含在一个synchronized(this)
块中,那么一个同步方法相当于一个同步块。因此,使用同步块的等效assign()
方法是,
public void assign(int i) {
synchronized(this) {
val = i;
}
}
您可以声明同步的静态方法。但是,获得锁的引用变量是什么呢?记住静态方法没有隐式的this
引用。静态同步方法获取class
对象上的锁。每个类都与一个Class
类型的对象相关联,您可以使用ClassName.class
语法来访问它。例如,
class SomeClass {
private static int val;
public static synchronized void assign(int i) {
val = i;
}
// more members ...
}
在这种情况下,assign
方法在被调用时获得了对SomeClass.class
对象的锁定。现在,使用同步块的等效assign()
方法可以写成
class SomeClass {
private static int val;
public static void assign(int i) {
synchronized(SomeClass.class) {
val = i;
}
}
// more members ...
}
不能声明同步的构造函数;这将导致编译器错误。例如,对于
class Synchronize {
public synchronized Synchronize() { /* constructor body */}
// more methods
}
您会得到以下错误:
Synchronize.java:2: modifier synchronized not allowed here
public synchronized Synchronize() { /* constructor body */}
为什么不能声明构造函数同步?JVM 确保在给定的时间点只有一个线程可以调用构造函数调用(对于特定的构造函数)。因此,没有必要声明一个同步的构造函数。但是,如果您愿意,可以在构造函数中使用同步块。
让我们回到清单 11-3 中的RaceCondition
例子。UseCounter
类中的increment()
方法也可以被重写为同步方法:
// declaring the increment synchronized instead of using
// a synchronized statement for a block of code inside the method
public synchronized void increment() {
Counter.count++;
System.out.print(Counter.count + " ");
}
该程序打印:
1 2 3 4 5 6 7 8 9
程序正确地打印出预期的输出。
初学者通常会误解同步块获得了代码块的锁。实际上,获得锁的是一个对象,而不是一段代码。获得的锁将一直保持到该块中的所有语句执行完毕。
线程问题
认证目标 | |
---|---|
识别死锁、饥饿、活锁和竞争条件中潜在的线程问题 |
线程中的并发编程充满了陷阱和问题。我们已经在前一节讨论了不使用锁时发生的竞争情况。在本节中,让我们讨论另外三个线程问题:死锁、饥饿和活锁。
僵局
获取和使用锁是很棘手的,这会导致很多问题。其中一个困难的(也是常见的)问题就是死锁。当锁定线程导致线程无法继续执行,从而无限期等待其他线程终止时,就会出现死锁。比方说,一个线程获取资源r1
上的锁,并等待获取另一个资源r2
上的锁。同时,假设有另一个线程已经获得了r2
,正在等待获得对r1
的锁。在另一个线程释放锁之前,任何一个线程都不能继续执行,而这是永远不会发生的——所以它们陷入了死锁。清单 11-4 展示了这种情况是如何发生的(使用板球比赛中的例子)。
Listing 11-4. DeadLock.java
// Balls class has a globally accessible data member to hold the number of balls thrown
class Balls {
public static long balls = 0;
}
// Runs class has a globally accessible data member to hold the number of runs scored
class Runs {
public static long runs = 0;
}
// Counter class has two methods – IncrementBallAfterRun and IncrementRunAfterBall.
// For demonstrating deadlock, we call these two methods in the run method, so that
// locking can be requested in opposite order in these two methods
class Counter implements Runnable {
// this method increments runs variable first and then increments the balls variable
// since these variables are accessible from other threads,
// we need to acquire a lock before processing them
public void IncrementBallAfterRun() {
// since we're updating runs variable first, first lock the Runs.class
synchronized(Runs.class) {
// lock on Balls.class before updating balls variable
synchronized(Balls.class) {
Runs.runs++;
Balls.balls++;
}
}
}
public void IncrementRunAfterBall() {
// since we're updating balls variable first; so first lock Balls.class
synchronized(Balls.class) {
// acquire lock on Runs.class before updating runs variable
synchronized(Runs.class) {
Balls.balls++;
Runs.runs++;
}
}
}
public void run() {
// call these two methods which acquire locks in different order
// depending on thread scheduling and the order of lock acquision,
// a deadlock may or may not arise
IncrementBallAfterRun();
IncrementRunAfterBall();
}
}
public class DeadLock {
public static void main(String args[]) throws InterruptedException {
Counter c = new Counter();
// create two threads and start them at the same time
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
System.out.println("Waiting for threads to complete execution...");
t1.join();
t2.join();
System.out.println("Done.");
}
}
如果您执行这个程序,程序可能会运行良好,也可能会死锁而永远不会终止(这个程序中死锁的发生取决于线程的调度方式)。
D:\> java DeadLock
Waiting for threads to complete execution...
Done.
D:\> java DeadLock
Waiting for threads to complete execution...
[deadlock – user pressed ctrl + c to terminate the program]
D:\> java DeadLock
Waiting for threads to complete execution...
Done.
在这个例子中,有两个类,Balls
和Runs
,它们的静态成员分别叫做balls
和runs
。Counter
类有两个方法,IncrementBallAfterRun()
和IncrementRunAfterBall()
。他们以相反的顺序获取Balls.class
和Runs.class
上的锁。run()
方法连续调用这两个方法。Dead
类中的main()
方法创建两个线程并启动它们。
当线程t1
和t2
执行时,它们调用方法IncrementBallAfterRun
和IncrementRunAfterBall
。在这些方法中,以相反的顺序获得锁。可能发生的情况是,t1
在Runs.class
获得一个锁,然后等待在Balls.class
获得一个锁。同时,t2
可能已经获得了Balls.class
,现在将等待获得对Runs.class
的锁定。因此,这个程序会导致死锁(图 11-2)。
不能保证每次执行这个程序都会导致死锁。为什么呢?您永远不知道线程执行的顺序以及获取和释放锁的顺序。正因如此,这样的问题就被说成是非确定性的,这样的问题是无法一致重现的。
有不同的策略来处理死锁,例如死锁预防、避免或检测。出于考试目的,以下是您需要了解的关于死锁的知识:
图 11-2。
Deadlock between threads t1 and t2
- 死锁可能在多重锁的上下文中出现。
- 如果以相同的顺序获得多个锁,则不会发生死锁;但是,如果您以不同的顺序获取它们,那么可能会出现死锁。
- 死锁(就像其他多线程问题一样)是非确定性的;您无法始终如一地重现死锁。
避免获取多个锁。如果您想要获得多个锁,请确保它们在任何地方都以相同的顺序获得,以避免死锁。
活锁
为了帮助理解活锁,让我们考虑一个类比。假设有两辆机器人汽车被编程为自动在道路上行驶。有一种情况,两辆机器人车到达一座窄桥的两个相对端。这座桥太窄了,一次只能有一辆汽车通过。机器人汽车被编程为等待另一辆汽车首先通过。当两辆车试图同时进入大桥时,可能会发生以下情况:每辆车开始进入大桥,注意到另一辆车也试图这样做,然后倒车!请注意,汽车一直向前和向后移动,因此看起来好像它们做了很多工作,但没有一辆车取得进展。这种情况称为活锁。
考虑两个线程t1
和t2
。假设线程t1
做出了改变,而线程t2
撤销了该改变。当线程t1
和t2
都工作时,看起来好像做了很多工作,但是没有进展。这种情况称为线程中的活锁。
活锁和死锁的相似之处在于进程“挂起”,程序永远不会终止。然而,在死锁中,线程停留在相同的状态,等待其他线程释放共享资源;在活锁中,线程不断执行任务,进程状态不断变化,但应用作为一个整体并没有进展。
锁饥饿
考虑这样一种情况,其中许多线程被分配了不同的优先级(从最低优先级 1 到最高优先级 10,这是 Java 中线程优先级的范围)。当锁可用时,线程调度器将优先考虑高优先级线程,而不是低优先级线程。如果有许多高优先级线程想要获得锁,并且长时间持有锁,那么低优先级线程什么时候有机会获得锁呢?换句话说,在低优先级线程长时间“饥饿”的情况下,试图获取锁被称为锁饥饿。
有许多技术可用于检测或避免线程问题,如活锁和饥饿,但它们不在 OCPJP 8 考试的范围内。从考试的角度来看,你应该知道我们在本章已经讨论过的不同种类的线程问题。
使用 java.util.concurrent.atomic 包
认证目标 | |
---|---|
使用 synchronized 关键字和 java.util.concurrent.atomic 包来控制线程执行的顺序 |
java.util.concurrent
包有两个子包:java.util.concurrent.atomic
和java.util.concurrent.locks
。在本节中,我们从 OCPJP 8 考试的角度讨论java.util.concurrent.atomic
包中的原子变量。
您经常可以看到获取和释放锁来实现基本/简单操作的代码,比如增加变量、减少变量等等?(在本章前面讨论synchronized
关键字时,我们已经看到了一个增加整数变量的例子。)为这种原始操作获取和释放锁是没有效率的。在这种情况下,Java 以原子变量的形式提供了一种有效的替代方法。
下面是这个包中的一些类及其简短描述的列表:
AtomicBoolean
:可原子更新的Boolean
值。AtomicInteger
:可原子更新的int
值;从Number
类继承而来。AtomicIntegerArray
:一个int
数组,其中的元素可以自动更新。AtomicLong
:可原子更新的long
值;继承自Number
类。AtomicLongArray
:一个long
数组,其中的元素可以自动更新。- 类型 v 的可原子更新的对象引用。
AtomicReferenceArray<E>
:一个可原子更新的数组,可以保存类型E
(E
指元素的基本类型)的对象引用。
只有
AtomicInteger
和AtomicLong
从Number
类扩展,而AtomicBoolean
没有。java.util.concurrent.atomic
子包中的所有其他类都直接继承自Object
类。
在java.util.concurrency.atomic
子包中的类中,AtomicInteger
和AtomicLong
是最重要的。表 11-2 列出了AtomicInteger
类中的重要方法。(AtomicLong
的方法与此类似。)
表 11-2。
Important Methods in the AtomicInteger Class
方法 | 简短描述 |
---|---|
AtomicInteger() |
创建一个初始值为 0 的AtomicInteger 实例。 |
AtomicInteger(int initVal) |
创建一个初始值为initVal 的AtomicInteger 实例。 |
int get() |
返回此对象中保存的整数值。 |
void set(int newVal) |
将该对象中保存的整数值重置为newVal 。 |
int getAndSet(int newValue) |
返回保存在该对象中的当前int 值,并将保存在该对象中的值设置为newVal 。 |
boolean compareAndSet(int expect, int update) |
将该对象的int 值与expect 值进行比较,如果它们相等,则将该对象的int 值设置为update 值。 |
int getAndIncrement() |
返回此对象中整数值的当前值,并递增此对象中的整数值。类似于i++ 的行为,其中i 是一个int 。 |
int getAndDecrement() |
返回此对象中整数值的当前值,并递减此对象中的整数值。类似于i-- 的行为,其中i 是一个int 。 |
int getAndAdd(int delta) |
返回保存在此对象中的整数值,并将给定的增量值与整数值相加。 |
int incrementAndGet() |
递增此对象中整数值的当前值,并返回该值。类似于++i 的行为,其中i 是一个int 。 |
int decrementAndGet() |
递减此对象中的当前整数值,并返回该值。类似于--i 的行为,其中i 是一个int 。 |
int addAndGet(int delta) |
将delta 值与该对象中整数的当前值相加,并返回该值。 |
int intValue() long longValue() float floatValue() double doubleValue() |
转换对象的当前int 值,并将其作为int 、long 、float 或double 值返回。 |
让我们尝试一个例子来理解如何使用AtomicInteger
或AtomicLong
。假设您有一个公共的计数器值,所有线程都可以访问它。如何安全地更新或访问这个公共计数器值,而不引入竞争条件问题(本章前面已经讨论过)?显然,您可以使用synchronized
关键字来确保临界区(修改计数器值的代码)在给定的时间点只被一个线程访问。临界截面将非常小,如
public void run() {
synchronized(SharedCounter.class) {
SharedCounter.count++;
}
}
然而,这段代码效率很低,因为它每次获取和释放锁只是为了增加count
的值。或者,如果您将count
声明为AtomicInteger
或AtomicLong
(以合适的为准)。像 AtomicInteger 这样的类不使用锁;相反,它们在内部使用可变变量和一种称为比较和设置(CAS)的底层机制。出于这个原因,使用 AtomicInteger 和相关类比使用使用synchronized
关键字的锁更快。
清单 11-5 展示了如何在实践中使用AtomicLong
。
Listing 11-5. AtomicVariableTest.java
import java.util.concurrent.atomic.AtomicInteger;
// Class to demonstrate how mutating "normal" (i.e., thread unsafe) integers
// and mutating "atomic" (i.e., thread safe) integers are different:
// Mutating a shared Integer object without locks can result in a race condition;
// however, mutating a shared AtomicInteger will not result in a race conditiond.
class Counter {
public static Integer``integer
public static AtomicInteger``atomicInteger
}
class AtomicVariableTest {
static class Incrementer extends Thread {
public void run() {
Counter.``integer
Counter.``atomicInteger
}
}
static class Decrementer extends Thread {
public void run() {
Counter.``integer
Counter.``atomicInteger
}
}
public static void main(String []args) throws InterruptedException {
Thread incremeterThread[] = new Incrementer[1000];
Thread decrementerThread[] = new Decrementer[1000];
for(int i = 0; i < 1000; i++) {
incremeterThread[i] = new Incrementer();
decrementerThread[i] = new Decrementer();
incremeterThread[i].start();
decrementerThread[i].start();
}
for(int i = 0; i < 1000; i++) {
incremeterThread[i].join();
decrementerThread[i].join();
}
System.``out
Counter.``integer``, Counter.``atomicInteger
}
}
实际输出取决于线程调度。在不同的运行中,它打印了以下输出:
Integer value = -2 AtomicInteger value = 0
Integer value = 2 AtomicInteger value = 0
Integer value = -1 AtomicInteger value = 0
Integer value = -1 AtomicInteger value = 0
Integer value = 0 AtomicInteger value = 0
我们来分析一下这个程序。Counter
类有两个数据成员——一个类型为Integer
,另一个类型为AtomicInteger
——具有相同的初始值 0。
有两个Thread
类。Incrementer
类有增加Integer
和AtomicInteger
值的run()
方法。另一方面,Decrementer
类有减少Integer
和AtomicInteger
值的run()
方法。
在这个输出中,注意递增Integer
对象可能会导致一个竞争条件:在递增和递减相同次数后,Integer
或AtomicInteger
的最终值应该总是 0——如果不是这样,我们就有一个竞争条件。从输出中可以观察到,有时对于Integer
对象它是 0(意味着没有竞争条件),但大多数时候它不等于 0(意味着它有竞争条件)。然而,对于AtomicInteger
,结果总是零(意味着它没有竞争条件)。换句话说,这个程序表明在没有任何锁的情况下操作一个AtomicInteger
值是安全的。
使用 java.util.concurrent 集合
认证目标 | |
---|---|
使用 java.util.concurrent 集合和类,包括 CyclicBarrier 和 CopyOnWriteArrayList |
在java.util.concurrent
包中有许多类和接口为并发编程提供高级 API。在这一节中,我们将主要讨论这个包中提供的同步器类。接下来,我们将简要介绍一下java.util.concurrent
包中提供的重要并发集合类。
您已经理解了底层并发结构,比如使用synchronized
关键字和使用Runnable
接口来创建线程。在共享资源需要被多个线程访问的情况下,对共享资源的访问和修改需要被保护。
当您使用synchronized
关键字时,您使用互斥体在线程之间同步,以实现安全的共享访问。线程还经常需要协调它们的执行来完成更大的更高级别的任务。可以为线程同步构建更高级别的抽象。这些用于同步两个或多个线程的活动的高级抽象被称为同步器。同步器在内部利用现有的低级 API 进行线程协调。
java.util.concurrent
库中提供的同步器及其用途如下:
- 一个
Semaphore
控制对共享资源的访问。信号量维护一个计数器来指定信号量控制的资源数量。 - 允许一个或多个线程等待倒计时完成。
Exchanger
类用于在两个线程之间交换数据。当两个线程需要相互同步并不断交换数据时,该类非常有用。CyclicBarrier
帮助提供一个同步点,线程可能需要在一个预定义的执行点等待,直到所有其他线程到达该点。- 当几个独立的线程必须分阶段工作来完成一项任务时,
Phaser
是一个有用的特性。
OCPJP 8 考试的目标只涵盖了CyclicBarrier
类,我们将在本节中借助一个例子来讨论它。
栅栏
在并发编程的许多情况下,线程可能需要在预定义的执行点等待,直到所有其他线程都到达该点。CyclicBarrier
帮助提供这样一个同步点;该类中的重要方法见表 11-3 。
表 11-3。
Important Methods in the CyclicBarrier Class
方法 | 简短描述 |
---|---|
CyclicBarrier(int numThreads) |
用指定的等待线程数创建一个CyclicBarrier 对象。如果numThreads 为负或零,抛出IllegalArgumentException 。 |
CyclicBarrier(int parties, Runnable barrierAction) |
与前面的构造函数相同;当到达障碍时,此构造函数还接受线程进行调用。 |
int await() int await(long timeout, TimeUnit unit) |
阻塞,直到指定数量的线程在此屏障上调用了await ()为止。方法返回该线程的到达索引。如果线程在等待其他线程时被中断,这个方法可以抛出一个InterruptedException ,或者如果由于某种原因(例如,另一个线程超时或中断)屏障被破坏,这个方法可以抛出一个BrokenBarrierException 。重载方法将超时期限作为附加选项;如果在超时期限内没有到达所有其他线程,这个重载版本抛出一个TimeoutException 。 |
boolean isBroken() |
如果屏障被破坏,则返回 true。如果屏障中至少有一个线程被中断或超时,或者如果屏障操作未能引发异常,则该屏障被破坏。 |
void reset() |
将屏障重置为初始状态。如果有任何线程在等待这个屏障,它们将抛出BrokenBarrier 异常。 |
清单 11-6 是利用CyclicBarrier
类的一个例子。
Listing 11-6. CyclicBarrierTest.java
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.BrokenBarrierException;
// The run() method in this thread should be called only when
// four players are ready to start the game
class MixedDoubleTennisGame extends Thread {
public void run() {
System.``out
}
}
// This thread simulates arrival of a player.
// Once a player arrives, he/she should wait for other players to arrive
class Player extends Thread {
CyclicBarrier waitPoint;
public Player(CyclicBarrier barrier, String name) {
this.setName(name);
waitPoint = barrier;
this.start();
}
public void run() {
System.``out
try {
waitPoint.await(); // await for all four players to arrive
} catch(BrokenBarrierException | InterruptedException exception) {
System.``out
+ exception);
}
}
}
// Creates a CyclicBarrier object by passing the number of threads and the thread to run
// when all the threads reach the barrier
class CyclicBarrierTest {
public static void main(String []args) {
// a mixed-double tennis game requires four players;
// so wait for four players
// (i.e., four threads) to join to start the game
System.``out
+ "As soon as four players arrive, game will start");
CyclicBarrier barrier = new CyclicBarrier(4, new MixedDoubleTennisGame());
new Player(barrier, "G I Joe");
new Player(barrier, "Dora");
new Player(barrier, "Tintin");
new Player(barrier, "Barbie");
}
}
该程序打印以下内容:
Reserving tennis court
As soon as four players arrive, game will start
Player Dora is ready
Player G I Joe is ready
Player Tintin is ready
Player Barbie is ready
All four players ready, game starts
Love all...
现在让我们看看这个程序是如何工作的。在main()
方法中,您创建了一个CyclicBarrier
对象。构造函数接受两个参数:要等待的线程数量,以及当所有线程都到达屏障时要调用的线程。在这种情况下,您有四个玩家要等待,因此您创建了四个线程,每个线程代表一个玩家。CyclicBarrier
构造函数的第二个参数是MixedDoubleTennisGame
对象,因为这个线程代表游戏,一旦四个玩家都准备好了,游戏就会开始。
在每个Player
线程的run()
方法中,调用CyclicBarrier
对象的await()
方法。一旦CyclicBarrier
对象的等待线程数达到 4,就会调用MixedDoubleTennisGame
中的run()
方法。
并发收款
java.util.concurrent
包提供了许多类,它们是java.util
包中集合框架类提供的线程安全等价类。例如,java.util.concurrent.ConcurrentHashMap
是java.util.HashMap
的并发等价物。这两个容器的主要区别在于,您需要用HashMap
显式地同步插入和删除,而这种同步内置在ConcurrentHashMap
中。如果你知道如何使用HashMap
,你就知道如何隐式地使用ConcurrentHashMap
。从 OCPJP 8 考试的角度来看,你只需要对表 11-4 中的类有一个整体的了解,所以我们不会深究如何利用这些类的细节。我们将只涵盖一个使用CopyOnWriteArrayList
的详细例子。
表 11-4。
Some Concurrent Collection Classes/Interfaces in the java.util.concurrent Package
类别/接口 | 简短描述 |
---|---|
BlockingQueue |
这个接口扩展了Queue 接口。在BlockingQueue 中,如果队列是空的,它等待(即阻塞)一个元素被插入,如果队列是满的,它等待一个元素被从队列中移除。 |
ArrayBlockingQueue |
这个类提供了基于固定大小数组的BlockingQueue 接口的实现。 |
LinkedBlockingQueue |
这个类提供了一个基于链表的接口实现。 |
DelayQueue |
这个类实现了BlockingQueue ,由类型为Delayed 的元素组成。只有在延迟期过后,才能从该队列中检索元素。 |
PriorityBlockingQueue |
相当于java.util.PriorityQueue ,但是实现了BlockingQueue 接口。 |
SynchronousQueue |
这个类实现了BlockingQueue 。在这个容器中,一个线程的每个insert ()等待(阻塞)另一个线程的相应的remove (),反之亦然。 |
LinkedBlockingDeque |
该类实现了插入和移除操作可能阻塞的地方的BlockingDeque ;使用链表来实现。 |
ConcurrentHashMap |
类似于Hashtable ,但是具有安全的并发访问和更新。 |
ConcurrentSkipListMap |
类似于TreeMap ,但是提供安全的并发访问和更新。 |
ConcurrentSkipListSet |
类似于TreeSet ,但是提供安全的并发访问和更新。 |
CopyOnWriteArrayList |
类似于ArrayList ,但提供安全的并发访问。当容器被修改时,它会创建一个基础数组的新副本。 |
CopyOnWriteArraySet |
一个Set 实现,但是提供安全的并发访问,并使用CopyOnWriteArrayList 实现。当容器被修改时,它会创建一个基础数组的新副本。 |
CopyOnWriteArrayList 类
ArrayList
和CopyOnWriteArrayList
都实现了List
接口。ArrayList
和CopyOnWriteArrayList
主要有三个区别:
ArrayList
不是线程安全的,但是CopyOnWriteArrayList
是线程安全的。这意味着,在多线程执行的环境中使用ArrayList
是不安全的(特别是当一些线程修改容器时),但是在这种环境中使用CopyOnWriteArrayList
是安全的。- 当一个线程正在访问
ArrayList
时,如果另一个线程修改了ArrayList
,那么ArrayList
中的方法如remove()
、add()
和set()
方法会抛出java.util.ConcurrentModificationException
。然而,在CopyOnWriteArrayList
中从多个线程执行这些操作是安全的,因此像remove()
、add()
和set()
这样的方法不会抛出这个异常。怎么做?所有的活动迭代器仍然可以访问容器的未修改版本,因此它们不受影响;如果您试图在修改后创建一个迭代器,您将获得修改后的容器的迭代器。 - 您可以通过在一个
List
对象上调用Iterator()
方法来获得一个迭代器。如果您在底层容器被修改时调用remove()
方法,您可能会得到一个ConcurrentModificationException
。然而,你不能在CopyOnWriteArrayList
的Iterator
上调用remove()
方法:它总是抛出UnsupportedOperationException
。
即使在不使用多线程的环境中,CopyOnWriteArrayList
的行为有时也很有用。例如,清单 11-7 显示了迭代器执行时被修改的ArrayList
。
Listing 11-7. ModifyingList.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ModifyingList {
public static void main(String []args) {
List<String> aList = new ArrayList<>();
aList.add("one");
aList.add("two");
aList.add("three");
Iterator listIter = aList.iterator();
while(listIter.hasNext()) {
System.out.println(listIter.next());
aList.add("four");
}
}
}
这个程序因抛出java.util.ConcurrentModificationException
而崩溃。为什么?因为ArrayList
的迭代器是快速失效的;当它遍历容器中的元素时,如果检测到底层容器发生了变化,就会抛出ConcurrentModificationException
而失败。当一个线程修改底层容器,而另一个线程遍历容器的元素时,这种行为在并发上下文中很有用。
当迭代发生时,您可以使用CopyOnWriteArrayList
对底层容器进行这样的更改。清单 11-8 是清单 11-7 的修改版本。这个版本用了一个CopyOnWriteArrayList
。
Listing 11-8. COWList.java
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class COWList {
public static void main(String []args) {
List<String> aList = new CopyOnWriteArrayList<>();
aList.add("one");
aList.add("two");
aList.add("three");
Iterator listIter = aList.iterator();
while(listIter.hasNext()) {
System.out.println(listIter.next());
aList.add("four");
}
}
}
现在程序没有崩溃,它打印:
one
two
three
注意,添加了三次的元素“four”没有作为输出的一部分打印出来。这是因为迭代器仍然可以访问包含三个元素的原始(未修改的)容器。如果你创建一个新的迭代器并访问元素,你会发现新元素已经被添加到aList
中。
使用可调用和执行服务接口
认证目标 | |
---|---|
使用 Runnable、Callable 创建工作线程,并使用 ExecutorService 并发执行任务 |
您可以通过创建Thread
对象来直接在应用中创建和管理线程。然而,如果你想抽象出多线程编程的底层细节,你可以利用Executor
接口。
图 11-3 显示了Executor
层次中的重要类和接口。在这一节中,您将重点关注使用Executor
和ExecutorService
接口。我们将在本章的后面讨论ForkJoinPool
。
图 11-3。
Important Classes/Interfaces in the Executor hierarchy
执行者
Executor
是一个只声明一个方法的接口:void execute(Runnable)
。这本身看起来可能不是一个重要的接口,但是它的派生类(或接口),比如ExecutorService, ThreadPoolExecutor
和ForkJoinPool
,支持有用的功能。我们将在本章后面更详细地讨论Executor
的一些派生类。现在,查看清单 11-9 中的一个简单的Executor
接口示例,了解如何实现这个接口并在实践中使用它。
Listing 11-9. ExecutorTest.java
import java.util.concurrent.Executor;
// This Task class implements Runnable, so its a Thread object
class Task implements Runnable {
public void run() {
System.``out
}
}
// This class implements Executor interface and should override execute(Runnable) method.
// We provide an overloaded execute method with an additional argument 'times' to create and
// run the threads for given number of times
class RepeatedExecutor implements Executor {
public void execute(Runnable runnable) {
new Thread(runnable).start();
}
public void execute(Runnable runnable, int times) {
System.``out
times);
for(int i = 0; i < times; i++) {
execute(runnable);
}
}
}
// This class spawns a Task thread and explicitly calls start() method.
// It also shows how to execute a Thread using Executor
class ExecutorTest {
public static void main(String []args) {
Runnable runnable = new Task();
System.``out
Thread thread = new Thread(runnable);
thread.start();
RepeatedExecutor executor = new RepeatedExecutor();
executor.execute(runnable, 3);
}
}
下面是这个程序的输出:
Calling Task.run() by directly creating a Thread
Calling Task.run()
Calling Task.run() 3 times thro' Executor.execute()
Calling Task.run()
Calling Task.run()
Calling Task.run()
在这个程序中,有一个通过提供run()
方法的定义来实现Runnable
的Task
类。类RepeatedExecutor
通过提供execute(Runnable)
方法的定义来实现Executor
接口。
Runnable
和Executor
在提供单一实现方法的意义上是相似的。在这个定义中,你可能已经注意到Executor
本身不是一个线程,你必须创建一个Thread
对象来执行在execute()
方法中传递的Runnable
对象。然而,Runnable
和Executor
的主要区别在于Executor
旨在抽象线程是如何执行的。例如,根据Executor
的实现,Executor
可以调度一个线程在某个时间运行,或者在某个延迟时间后执行该线程。
在这个程序中,您已经用一个额外的参数重载了execute()
方法,以创建和执行线程一定的次数。在main()
方法中,首先创建一个Thread
对象并调度它运行。之后,实例化RepeatedExecutor
来执行线程三次。
可调用和可执行服务
Callable
是一个只声明一个方法的接口:call()
。它的全称是V call() throws Exception
。它表示需要由线程完成的任务。一旦任务完成,它将返回值。出于某种原因,如果call()
方法无法执行或失败,它会抛出一个Exception
。
要使用Callable
对象执行任务,首先要创建一个线程池。线程池是可以执行任务的线程的集合。使用Executors
实用程序类创建一个线程池。这个类提供了获取线程池、线程工厂等实例的方法。
ExecutorService
接口扩展了Executor
接口,提供了终止线程和产生Future
对象等服务。某些任务可能需要相当长的执行时间才能完成。因此,当您向 executor 服务提交任务时,您会得到一个Future
对象。
Future
表示包含由线程在未来返回的值的对象(即,一旦线程在“未来”终止,它就返回该值)。您可以使用Future
类中的isDone()
方法来检查任务是否完成,然后使用get()
方法来获取任务结果。如果在任务未完成时直接调用get()
方法,该方法会一直阻塞,直到任务完成并返回可用的值。
这里有一个简单的例子来看看这些类是如何一起工作的(清单 11-10 )。
Listing 11-10. CallableTest.java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.Executors;
// Factorial implements Callable so that it can be passed to a ExecutorService
// and get executed as a task.
class Factorial implements Callable<Long> {
long n;
public Factorial(long n) {
this.n = n;
}
public Long call() throws Exception {
if(n <= 0) {
throw new Exception("for finding factorial, N should be > 0");
}
long fact = 1;
for(long longVal = 1; longVal <= n; longVal++) {
fact *= longVal;
}
return fact;
}
}
// Illustrates how Callable, Executors, ExecutorService, and Future are related;
// also shows how they work together to execute a task
class CallableTest {
public static void main(String []args) throws Exception {
// the value for which we want to find the factorial
long N = 20;
// get a callable task to be submitted to the executor service
Callable<Long> task = new Factorial(N);
// create an ExecutorService with a fixed thread pool having one thread
ExecutorService es = Executors.newSingleThreadExecutor();
// submit the task to the executor service and store the Future object
Future<Long> future = es.submit(task);
// wait for the get() method that blocks until the computation is complete.
System.out.printf("factorial of %d is %d", N, future.get());
// done. shutdown the executor service since we don't need it anymore
es.shutdown();
}
}
该程序打印以下内容:
factorial of 20 is 2432902008176640000
在这个程序中,您有一个实现了Callable
的Factorial
类。因为任务是计算一个数字N
的阶乘,所以任务需要返回一个结果。您使用Long
类型作为阶乘值,因此您实现了Callable<Long>
。在Factorial
类中,您定义了实际执行任务的call()
方法(这里的任务是计算给定数字的阶乘)。如果给定值N
为负或零,则不执行任务并向调用者抛出一个异常。否则,您将从 1 循环到N
并找到阶乘值。
在CallableTest
类中,首先创建一个Factorial
类的实例。然后,您需要执行这个任务。为了简单起见,通过调用Executors
类中的newSingleThreadExecutor()
方法,可以得到一个单线程执行器。请注意,您可以使用其他方法,如newFixedThreadPool(nThreads)
,根据您需要的并行级别创建一个多线程的线程池。
一旦您获得了一个ExecutorService
,您就提交任务来执行。ExecutorService
抽象出任务何时执行、任务如何分配给线程等细节。当你调用submit(task)
方法时,你得到了对Future<Long>
的引用。从这个未来的引用中,您可以调用get()
方法在完成任务后获取结果。如果当您调用future.get()
时任务仍在执行,这个get()
方法将阻塞,直到任务执行完成。一旦执行完成,您需要通过调用shutdown()
方法来手动释放ExecutorService
。
使用并行 Fork/Join 框架
认证目标 | |
---|---|
使用并行 Fork/Join 框架 |
java.util.concurrent
包中的 Fork/Join 框架有助于简化并行代码的编写。该框架是ExecutorService
接口的一个实现,提供了一个易于使用的并发平台,以便利用多个处理器。这个框架对于建模分治问题非常有用。这种方法适用于可以递归划分并在较小规模上计算的任务;然后合并计算结果。将任务分成更小的任务是分叉,将更小任务的结果合并是连接。
Fork/Join 框架使用工作窃取算法:当一个worker
线程完成了它的工作并且空闲时,它从其他仍在忙于做一些工作的线程那里获取(或“窃取”)工作。最初,您会觉得使用 Fork/Join 是一项复杂的任务。然而,一旦你熟悉了它,你会意识到它在概念上是容易的,并且它极大地简化了你的工作。关键是递归地将任务细分成更小的块,这些块可以由单独的线程处理。
简而言之,Fork/Join 算法设计如下:
forkJoinAlgorithm() {
fork (split) the tasks;
join the tasks;
compose the results;
}
以下是这些步骤如何工作的伪代码:
doRecursiveTask(input) {
if (the task is small enough to be handled by a thread) {
compute the small task;
if there is a result to return, do so
}
else {
divide (i.e., fork) the task into two parts
call compute() on first task, join() on second task, return combined results
}
}
图 11-4 显示了任务如何被递归地细分成更小的任务,以及部分结果如何被组合。如图所示,一个任务被分割成两个子任务,然后每个子任务又被分割成两个子任务,依此类推,直到每个分割的子任务可被每个线程计算。一旦一个线程完成了计算,它就返回与其他结果相结合的结果;以这种方式,所有的计算结果被组合回来。
图 11-4。
The Fork/Join Framework Uses Divide-and-Conquer to Complete the Task
Fork/Join 框架中有用的类
下面的类在 Fork/Join 框架中扮演着重要的角色:ForkJoinPool
、ForkJoinTask
、RecursiveTask
和RecursiveAction
。让我们更详细地考虑这些类。
-
ForkJoinPool
is the most important class in the Fork/Join framework. It is a thread pool for running fork/join tasks and it executes an instance ofForkJoinTask
. It executes tasks and manages their lifecycle. Table 11-5 lists the important methods belonging to this abstract class.表 11-5。
Important Methods in the ForkJoinPool Class
方法 简短描述 见执行(forkointask>任务) 异步执行给定的任务。 T 调用(forkjointtask 任务) 执行给定的任务并返回计算结果。 列表 > invokeAll(集合 extends Callable >任务) 执行所有给定的任务,并在所有任务完成后返回未来对象的列表。 布尔值被终止() 如果所有任务都已完成,则返回 true。 int getparallelism()int getpoosize()long getstealcount()int get active thread count() 这些是状态检查方法。 forkointask 提交(安静 任务】 forkointask 提交(forkointask 任务)>提交(Runnable 任务) forkointask 提交(Runnable 任务,T result) 这些方法正在执行提交的任务。重载版本接受不同类型的任务;返回任务对象或未来对象。 -
ForkJoinTask<V>
是一个轻量级的类似线程的实体,代表一个定义了fork()
和join()
等方法的任务。表 11-6 列出了这个类的重要方法。
表 11-6。
Important Methods in the ForkJoinTask Class
方法 | 简短描述 |
---|---|
boolean cancel(boolean mayInterruptIfRunning) |
尝试取消任务的执行。 |
ForkJoinTask<V> fork() |
异步执行任务。 |
V join() |
当计算完成时,返回计算结果。 |
V get() |
返回计算结果;如果计算未完成,则等待。 |
V invoke() static <T extends ForkJoinTask<?>> Collection<T> invokeAll(Collection<T> tasks) |
开始执行提交的任务;等待计算完成,并返回结果。 |
boolean isCancelled() |
如果任务被取消,则返回true 。 |
boolean isDone() |
如果任务完成,则返回true 。 |
RecursiveTask<V>
是可以在ForkJoinPool
中运行的任务;compute()
方法返回类型 v 的值。它继承自ForkJoinTask
。RecursiveAction
是可以在ForkJoinPool
中运行的任务;它的compute()
方法执行任务中的实际计算步骤。它类似于RecursiveTask
,但不返回值。
使用 Fork/Join 框架
让我们确定如何在解决问题时使用 Fork/Join 框架。以下是使用该框架的步骤:
- 首先检查问题是否适合 Fork/Join 框架。记住:Fork/Join 框架并不适合所有类型的任务。如果您的问题符合以下描述,此框架是合适的:
- 这个问题可以被设计成一个递归任务,任务可以被细分成更小的单元,结果可以组合在一起。
- 细分的任务是独立的,并且可以单独计算,而不需要在计算过程中任务之间的通信。(当然,在计算结束后,您需要将它们连接在一起。)
- 如果您想要解决的问题可以递归建模,那么定义一个扩展
RecursiveTask
或RecursiveAction
的任务类。如果任务返回结果,从RecursiveTask
扩展;否则从RecursiveAction
开始延伸。 - 在新定义的任务类中覆盖
compute()
方法。如果任务足够小,可以执行,compute()
方法实际执行任务;或者将任务分成子任务并调用它们。子任务可以通过invokeAll()
或fork()
方法调用(当子任务返回值时使用fork()
)。使用join()
方法获得计算结果(如果您之前使用了fork()
方法)。 - 如果从子任务计算,合并结果。
- 然后实例化
ForkJoinPool
,创建一个task
类的实例,并在ForkJoinPool
实例上使用invoke()
方法开始执行任务。 - 就这样——你完成了。
现在让我们试着解决如何对 1 求和的问题..其中 N 是一个大数字。你可以使用 Fork/Join 框架来解决这个问题(清单 11-11 )。
Listing 11-11. SumOfNUsingForkJoin.java
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
// This class illustrates how we can compute sum of 1..N numbers using fork/join framework.
// The range of numbers are divided into half until the range can be handled by a thread.
// Once the range summation completes, the result gets summed up together.
class SumOfNUsingForkJoin {
private static long N = 1000_000; // one million - we want to compute sum
// from 1 .. one million
private static final int NUM_THREADS = 10;
// number of threads to create for
// distributing the effort
// This is the recursive implementation of the algorithm; inherit from RecursiveTask
// instead of RecursiveAction since we're returning values.
static class RecursiveSumOfN extends RecursiveTask<Long> {
long from, to;
// from and to are range of values to sum-up
public RecursiveSumOfN(long from, long to) {
this.from = from;
this.to = to;
}
// the method performs fork and join to compute the sum if the range
// of values can be summed by a threadremember that we want to divide
// the summation task equally among NUM_THREADS) then, sum the range
// of numbers from..to using a simple for loop;
// otherwise, fork the range and join the results
public Long compute() {
if( (to - from) <= N/NUM_THREADS) {
// the range is something that can be handled
// by a thread, so do summation
long localSum = 0;
// add in range 'from' .. 'to' inclusive of the value 'to'
for(long i = from; i <= to; i++) {
localSum += i;
}
System.out.printf("\tSum of value range %d to %d is %d %n",
from, to, localSum);
return localSum;
}
else {
// no, the range is too big for a thread to handle,
// so fork the computation
// we find the mid-point value in the range from..to
long mid = (from + to)/2;
System.out.printf("Forking computation into two ranges: " +
// determine the computation for first half
// with the range from..mid
RecursiveSumOfN firstHalf = new RecursiveSumOfN(from, mid);
// now, fork off that task
firstHalf.fork();
// determine the computation for second half
// with the range mid+1..to
RecursiveSumOfN secondHalf
= new RecursiveSumOfN(mid + 1, to);
long resultSecond = secondHalf.compute();
// now, wait for the first half of computing sum to
// complete, once done, add it to the remaining part
return firstHalf.join() + resultSecond;
}
}
}
public static void main(String []args) {
// Create a fork-join pool that consists of NUM_THREADS
ForkJoinPool pool = new ForkJoinPool(NUM_THREADS);
// submit the computation task to the fork-join pool
long computedSum = pool.invoke(new RecursiveSumOfN(0, N));
// this is the formula sum for the range 1..N
long formulaSum = (N * (N + 1)) / 2;
// Compare the computed sum and the formula sum
System.out.printf("Sum for range 1..%d; computed sum = %d, " +
"formula sum = %d %n", N, computedSum, formulaSum);
}
}
该程序打印以下内容:
Forking computation into two ranges: 0 to 500000 and 500000 to 1000000
Forking computation into two ranges: 500001 to 750000 and 750000 to 1000000
Forking computation into two ranges: 0 to 250000 and 250000 to 500000
Forking computation into two ranges: 500001 to 625000 and 625000 to 750000
Forking computation into two ranges: 750001 to 875000 and 875000 to 1000000
Forking computation into two ranges: 500001 to 562500 and 562500 to 625000
Forking computation into two ranges: 625001 to 687500 and 687500 to 750000
Forking computation into two ranges: 0 to 125000 and 125000 to 250000
Forking computation into two ranges: 250001 to 375000 and 375000 to 500000
Sum of value range 562501 to 625000 is 37109406250
Forking computation into two ranges: 0 to 62500 and 62500 to 125000
Sum of value range 687501 to 750000 is 44921906250
Forking computation into two ranges: 250001 to 312500 and 312500 to 375000
Forking computation into two ranges: 750001 to 812500 and 812500 to 875000
Sum of value range 250001 to 312500 is 17578156250
Forking computation into two ranges: 875001 to 937500 and 937500 to 1000000
Sum of value range 750001 to 812500 is 48828156250
Sum of value range 812501 to 875000 is 52734406250
Sum of value range 312501 to 375000 is 21484406250
Forking computation into two ranges: 125001 to 187500 and 187500 to 250000
Sum of value range 625001 to 687500 is 41015656250
Forking computation into two ranges: 375001 to 437500 and 437500 to 500000
Sum of value range 187501 to 250000 is 13671906250
Sum of value range 62501 to 125000 is 5859406250
Sum of value range 500001 to 562500 is 33203156250
Sum of value range 437501 to 500000 is 29296906250
Sum of value range 125001 to 187500 is 9765656250
Sum of value range 875001 to 937500 is 56640656250
Sum of value range 0 to 62500 is 1953156250
Sum of value range 937501 to 1000000 is 60546906250
Sum of value range 375001 to 437500 is 25390656250
Sum for range 1..1000000; computed sum = 500000500000, formula sum = 500000500000
我们来分析一下这个程序是如何工作的。在这个程序中,您希望计算范围 1 中的值的总和..1,000,000.为了简单起见,您决定使用十个线程来执行任务。类RecursiveSumOfN
扩展了RecursiveTask<Long>
。在RecursiveTask<Long>
中,您使用<Long>
,因为每个子范围中的数字之和是一个Long
值。此外,您选择了RecursiveTask<Long>
而不是普通的RecursiveAction
,因为每个子任务都返回一个值。如果子任务没有返回值,可以使用RecursiveAction
来代替。
在compute()
方法中,您决定是计算范围的总和还是使用以下条件进一步细分任务:
(to - from) <= N/NUM_THREADS)
您在这个计算中使用这个“阈值”。换句话说,如果值的范围在任务可以处理的阈值内,那么您执行计算;否则,你递归地将任务分成两部分。您使用一个简单的for
循环来查找该范围内的值的总和。在另一种情况下,您划分范围的方式类似于您在二分搜索法算法中划分范围的方式:对于范围from .. to
,您找到中点并创建两个子范围from .. mid
和mid + 1 .. to
。一旦您调用了fork()
,您就等待第一个任务完成总和的计算,并为后半部分的计算生成另一个任务。
在main()
方法中,您创建一个ForkJoinPool
,线程数由NUM_THREADS
给出。您将任务提交到 fork/join 池,并获得 1 的计算总和..1,000,000.现在,您还可以使用公式计算 N 个连续数字的总和。
从程序的输出中,您可以观察到任务是如何被细分为子任务的。您还可以从输出中验证计算的总和与从公式中计算的总和是否相同,这表明您对子范围求和的任务划分是正确的。
在这个程序中,您任意假设要使用的线程数量是 10 个。这是为了简化这个程序的逻辑。确定阈值的一个更好的方法是用数据长度除以可用处理器的数量。换句话说,
threshold value
= (data length size) / (number of available processors);
如何以编程方式获得可用处理器的数量?为此,您可以使用方法Runtime.getRuntime().availableProcessors())
。
在清单 11-11 中,你用了RecursiveTask
;但是,如果任务没有返回值,那么您应该使用RecursiveAction
。如果你用RecursiveAction
而不是RecursiveTask
,程序会有一些不同。一个变化是你需要从RecursiveAction
扩展任务类。同样,compute()
方法不返回任何东西。另一个变化是您需要使用invokeAll()
方法来提交要执行的子任务。最后,一个明显的变化是,您需要在compute()
方法中执行搜索,而不是在前面的情况中进行求和。
要记住的要点
为你的 OCPJP 八级考试记住这些要点:
- 使用基本的并发结构,如
start()
和join()
,可以实现 Fork/Join 框架所提供的功能。然而,Fork/Join 框架抽象了许多底层细节,因此更易于使用。此外,使用 Fork/Join 框架比在较低级别处理线程更有效。此外,使用ForkJoinPool
可以有效地管理线程,性能比传统的线程池好得多。出于所有这些原因,我们鼓励您使用 Fork/Join 框架。 - Fork/Join 框架中的每个
worker
线程都有一个工作队列,使用一个Deque
来实现。每次创建新任务(或子任务)时,它都被推到自己队列的前面。当一个任务完成了一个任务,并执行了一个与另一个尚未完成的任务的连接时,它很聪明。线程从队列头弹出一个新任务,开始执行而不是休眠(为了等待另一个任务完成)。事实上,如果一个线程的队列是空的,那么这个线程从属于另一个线程的队列尾部弹出一个任务。这只不过是一个偷工减料的算法。 - 显而易见的是,为两个子任务调用
fork()
(如果你要分成两个子任务),并调用两次join()
。这是正确的——但效率低下。为什么呢?嗯,基本上你正在创建比有用的更多的并行任务。在这种情况下,原始线程将等待其他两个任务完成,考虑到任务创建成本,这是低效的。这就是为什么你一次调用fork()
而第二次调用compute()
的原因。 fork()
和join()
调用的放置非常重要。例如,让我们假设您按以下顺序进行调用:first.fork();
resultFirst = first.join();
resultSecond = second.compute();
这种用法是两个任务的串行执行,因为第二个任务仅在第一个任务完成后才开始执行。因此,它甚至比它的顺序版本效率更低,因为这个版本还包括任务创建的成本。要点:注意你的 fork/join 调用的位置。- 使用 Fork/Join 框架时,性能并不总是有保证的。我们前面提到的原因之一是 fork/join 调用的位置。
使用并行流
认证目标 | |
---|---|
使用并行流,包括缩减、分解、合并流程、管道和性能 |
流可以是顺序的,也可以是并行的。当我们讨论流 API 时(在第六章中的,我们只讨论了顺序流。在本节中,让我们讨论并行流。
什么是平行流?并行流将元素分成多个块,用不同的线程处理每个块,并且(如果必要的话)组合来自这些线程的结果以评估最终结果。
在上一节中,我们讨论了 fork/join 框架:通过递归地将任务分割成子任务来执行任务,然后并行执行子任务。并行流在内部使用这个 fork/join 框架。流程步骤应该由无状态和独立的任务组成。
这是一个从 1 到 N 计算素数的例子。用于检查一个给定的数是否是直截了当的逻辑是,我们检查是否有任何数可以从 2 到 N/2 整除。当然,我们可以简化逻辑来加速计算,但我们的目标是展示并行流是如何工作的,所以我们保留了使用这个简单的逻辑来检查给定的数是否是质数。首先,让我们看看这个程序的顺序版本(清单 11-12 )。
Listing 11-12. PrimeNumbers.java
import java.util.stream.LongStream;
class PrimeNumbers {
private static boolean isPrime(long val) {
for(long i = 2; i <= val/2; i++) {
if((val % i) == 0) {
return false;
}
}
return true;
}
public static void main(String []args) {
long numOfPrimes = LongStream.rangeClosed(2, 100_000)
.filter(PrimeNumbers::isPrime)
.count();
System.out.println(numOfPrimes);
}
}
该程序打印:
9592
这个程序正确地报告了 100,000 之前有 9,592 个质数。当我们对它进行计时时,运行时间为 2.510 秒(在我的配有 2.4 GHz 英特尔酷睿 i5 双核处理器的机器上)。
并行计算非常容易:我们必须调用LongStream
接口中提供的parallel()
方法。有此变化的代码段是:
long numOfPrimes = LongStream.rangeClosed(2, 100_000)
.parallel()
.filter(PrimeNumbers::isPrime)
.count();
System.out.println(numOfPrimes);
由于对parallel()
的调用,流变成了并行流,要执行的工作被拆分并分派给 fork/join 池中可用的线程来执行。当并行执行素数个数的计算时,现在花费的时间减少到 1.235 秒。与在顺序流中执行计算花费的 2.510 秒相比,这几乎是花费时间的一半。
如果对比使用 fork/join 框架的代码的复杂度(查看清单 11-9 中的代码示例),使用并行流的代码非常简单:我们所要做的只是在流中调用parallel()
方法!
当你调用
Collection
类的stream()
方法时,你将得到一个连续的流。当你调用Collection
类的parallelStream()
方法时,你会得到一个并行流。
您可以通过调用isParallel()
方法来检查流是顺序的还是并行的。下面是一段简单的代码,演示了如何在流中使用 is 方法:
System.out.println(IntStream.range(1, 10).filter(i -> (i % 2) == 0).isParallel());
这段代码显示:false。为什么呢?因为底层流(默认情况下)是顺序的,因此isParallel()
方法返回false
。这段代码怎么样?
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
System.out.println(ints.parallelStream().filter(i -> (i % 2) == 0).isParallel());
因为底层流是并行的(因为parallelStream()
方法调用),所以isParallel()
方法返回true
。
您可以通过调用
parallel()
方法将顺序流转换为并行流;类似地,您可以通过调用sequential()
方法将并行流转换为顺序流。
这段代码会输出什么?
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
System.out.println(ints.parallelStream().filter(i -> (i % 2) == 0).sequential().isParallel());
这段代码显示:false。为什么呢?虽然创建的流是一个并行流,但是对sequential()
方法的调用使得流是连续的。因此,调用isParallel()
打印false
。
执行正确的缩减
为了正确使用并行流,重要的是不要依赖全局状态。换句话说,计算应该没有“副作用”。为了给出错误使用流的例子,这里有一个例子(清单 11-13 )。
Listing 11-13. StringSplitAndConcatenate.java
import java.util.Arrays;
class StringConcatenator {
public static String result = "";
public static void concatStr(String str) {
result = result + " " + str;
}
}
class StringSplitAndConcatenate {
public static void main(String []args) {
String words[] = "the quick brown fox jumps over the lazy dog".split(" ");
Arrays.stream(words).forEach(StringConcatenator::concatStr);
System.out.println(StringConcatenator.result);
}
}
该程序打印:
the quick brown fox jumps over the lazy dog
在这个程序中,我们将拆分字符串"the quick brown fox jumps over the lazy dog"
中的单词,然后再次组合。为了组合单词,我们使用一个全局变量结果,并通过在流的forEach()
方法中传递StringConcatenator::concatStr()
方法引用来修改它。因为底层流是一个顺序流,所以我们似乎不会遇到麻烦,并且我们能够正确地重建字符串。然而,这里有一个程序的修改版本,它通过调用parallel()
将流转换为并行流。
Arrays.stream(words).``parallel()
这一个变化,我们得到了乱码字符串!当我们运行这个程序时,它打印出:
quick the fox brown lazy dog the jumps
当我们再次运行它时,它打印出来:
fox quick the jumps lazy dog
很明显,当我们使用parallel()
方法时,出现了严重的问题。发生什么事了?
当流是并行的时,任务被分成多个子任务,不同的线程执行它。对forEach(StringConcatenator::concatStr)
的调用现在访问了StringConcatenator
类中的全局可访问变量 result。因此,这个程序存在一个竞争条件问题(在本章前面已经讨论过)。我们如何解决这个问题?我们需要摆脱修改全局状态,并保持减少局部化。我们可以使用reduce()
方法来代替,如清单 11-14 所示。请记住,当您想要在流元素上执行重复操作来计算结果时,可以在流上使用reduce()
方法。
Listing 11-14. CorrectStringSplitAndConcatenate.java
import java.util.Arrays;
import java.util.Optional;
class CorrectStringSplitAndConcatenate {
public static void main(String []args) {
String words[] = "the quick brown fox jumps over the lazy dog".split(" ");
Optional<String> originalString =
(Arrays.stream(words).parallel().reduce((a, b) -> a + " " + b));
System.out.println(originalString.get());
}
}
该程序可以正确打印:
the quick brown fox jumps over the lazy dog
如果您移除或保留parallel()
方法(这并不重要),这个程序将正确地连接单词以打印原始字符串,因为我们已经正确地使用了 reduce 操作,而不依赖于全局状态变化。
并行流和性能
使用并行流的一个重要注意事项是:并行流的性能并不总是比顺序流好。只有当操作是在大量元素上执行的,操作的计算开销很大,并且数据结构是可有效拆分的,您才会看到并行流的性能提高;否则,并行流的执行速度可能会比顺序流慢!
默认情况下,fork/join 线程池的线程数通常等于机器中的处理器数。您可以使用这个调用:Runtime.getRuntime().availableProcessors()
获得您机器中的处理器数量。这个默认配置对于并行流的大多数使用来说已经足够好了。或者,您可以使用ForkJoinPool
中的getParallelism()
方法检查默认并行度:
System.out.println(ForkJoinPool.commonPool().getParallelism());
// it printed 3 in my machine
getParallelism()
方法从系统属性java.util.concurrent.ForkJoinPool.common.parallelism
中获取值。您可以使用System.setProperty
方法来修改这个系统属性的值(清单 11-15 )。
Listing 11-15. Parallelism.java
import java.util.concurrent.ForkJoinPool;
public class Parallelism {
public static void main(String []args) {
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8");
System.out.println(ForkJoinPool.commonPool().getParallelism());
}
}
当执行时,这个程序打印:8。使用System.setProperty()
的另一种方法是在调用 JVM 时,通过将它作为 JVM 参数传递来设置该属性,如下所示:
java -Djava.util.concurrent.ForkJoinPool.common.parallelism=8 GetParallelism
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
使用 Runnable、Callable 创建工作线程,并使用 ExecutorService 并发执行任务
- 您可以通过实现
Runnable
接口或扩展Thread
类来创建支持多线程的类。 - 始终实现
run()
方法。在Thread
中默认的run()
方法什么都不做。 - 在代码中直接调用
start()
方法,而不是run()
方法。(让 JVM 调用run()
方法。) Callable
接口代表一个需要由线程完成的任务。一旦任务完成,一个Callable
实现的call()
方法就会返回值。Executor
层次抽象了多线程编程的底层细节,并提供了高层用户友好的并发结构。
识别死锁、饥饿、活锁和竞争条件中潜在的线程问题
- 对资源的并发读取和写入可能会导致竞争条件问题。
- 您必须使用线程同步(即锁)来访问共享值并避免竞争情况。Java 提供了线程同步特性,以提供对共享资源的受保护访问——即同步块和同步方法。
- 使用锁会带来死锁等问题。当死锁发生时,进程将挂起,并且永远不会终止。
- 当两个线程以相反的顺序获取锁时,通常会发生死锁。当一个线程获得了一个锁并等待另一个锁时,另一个线程获得了另一个锁并等待第一个锁被释放。因此,没有取得任何进展,程序死锁。
- 当一个线程所做的更改被另一个线程重复撤消时,两个线程都很忙,但是应用作为一个整体没有进展;这种情况称为活锁。
- 低优先级线程长时间“饥饿”试图获取锁的情况称为锁饥饿。
使用 synchronized 关键字和 java.util.concurrent.atomic 包来控制线程执行的顺序
- 在 synchronized 块中,使用 synchronized 关键字作为引用变量,并在其后跟随一个代码块。线程必须获得同步变量的锁才能进入该块;当块的执行完成时,线程释放锁。
- Java 以原子变量的形式提供了一种有效的替代方法,在这种情况下,只需要获取和释放一个锁就可以对变量执行基本操作。
- 锁确保一次只有一个线程访问共享资源。
- 为在基元类型上执行操作而执行锁定和解锁是低效的。更好的选择是使用
java.util.concurrent.atomic
包中提供的原子变量,包括AtomicBoolean
、AtomicInteger
、AtomicIntegerArray
、AtomicLong
、AtomicLongArray
、AtomicReference<V>
和AtomicReferenceArray<E>
。
使用 java.util.concurrent 集合和类,包括 CyclicBarrier 和 CopyOnWriteArrayList
Semaphore
控制对一个或多个共享资源的访问。CountDownLatch
允许线程等待倒计时完成。Exchanger
支持两个线程之间交换数据。Phaser
用于支持同步屏障。CyclicBarrier
允许线程在预定义的执行点等待。java.util.concurrent
包提供了许多类,它们是java.util
包中集合框架类提供的线程安全等价类;例如,java.util.concurrent.ConcurrentHashMap
是java.util.HashMap
的并发等价物。CopyOnWriteArrayList
类似于ArrayList
,但是提供安全的并发访问。当您修改一个CopyOnWriteArrayList
时,会创建一个底层数组的新副本。
使用并行 Fork/Join 框架
- Fork/Join 框架是一种以相当好的并行性执行程序的可移植方法。
- 该框架是
ExecutorService
接口的一个实现,提供了一个易于使用的并发平台,以便利用多个处理器。 - 这个框架对于建模分治问题非常有用。
- Fork/Join 框架使用工作窃取算法:当一个工作线程完成其工作并空闲时,它从仍在忙于做某些工作的其他线程那里获取(或“窃取”)工作。
- 工作窃取技术以最小的同步成本实现了良好的负载平衡线程管理。
ForkJoinPool
是 Fork/Join 框架中最重要的类。它是一个用于运行 fork/join 任务的线程池——它执行一个ForkJoinTask
实例。它执行任务并管理它们的生命周期。ForkJoinTask<V>
是一个轻量级的类似线程的实体,代表一个定义了fork()
和join()
等方法的任务。
使用并行流,包括缩减、分解、合并流程、管道和性能
- 并行流将元素分成多个块,用不同的线程处理每个块,并且(如果必要的话)组合来自这些线程的结果以评估最终结果。
- 当您调用
Collection
类的stream()
方法时,您将获得一个顺序流。当你调用Collection
类的parallelStream()
方法时,你会得到一个并行流。 - 并行流在内部使用 fork/join 框架。为了正确使用并行流,流程步骤应该由无状态和独立的任务组成。
- 您可以通过调用
parallel()
方法将顺序流转换为并行流;类似地,您可以通过调用sequential()
方法将并行流转换为顺序流。 - 您可以通过调用
isParallel()
方法来检查流是顺序的还是并行的。
Q0075estion TimeHere is a class named PingPong that extends the Thread class. Which of the following PingPong class implementations correctly prints “ping” from the worker thread and then prints “pong” from the main thread? class PingPong extends Thread {
public void run() {
System.out.println("ping ");
}
public static void main(String []args) {
Thread pingPong = new PingPong();
System.out.print("pong");
}
}
class PingPong extends Thread {
public void run() {
System.out.println("ping ");
}
public static void main(String []args) {
Thread pingPong = new PingPong();
pingPong.run();
System.out.print("pong");
}
}
class PingPong extends Thread {
public void run() {
System.out.println("ping");
}
public static void main(String []args) {
Thread pingPong = new PingPong();
pingPong.start();
System.out.println("pong");
}
}
class PingPong extends Thread {
public void run() {
System.out.println("ping");
}
public static void main(String []args) throws InterruptedException{
Thread pingPong = new PingPong();
pingPong.start();
pingPong.join();
System.out.println("pong");
}
}
You’ve written an application for processing tasks. In this application, you’ve separated the critical or urgent tasks from the ones that are not critical or urgent. You’ve assigned high priority to critical or urgent tasks. In this application, you find that the tasks that are not critical or urgent are the ones that keep waiting for an unusually long time. Since critical or urgent tasks are high priority, they run most of the time. Which one of the following multi-threading problems correctly describes this situation? Deadlock Starvation Livelock Race condition Which of the following two definitions of Sync (when compiled in separate files) will compile without errors? class Sync {
public synchronized void foo() {}
}
abstract class Sync {
public synchronized void foo() {}
}
abstract class Sync {
public abstract synchronized void foo();
}
interface Sync {
public synchronized void foo();
}
Consider the following program: import java.util.concurrent.atomic.*;
class AtomicIntegerTest {
static AtomicInteger ai = new AtomicInteger(10);
public static void check() {
assert (ai.intValue() % 2) == 0;
}
public static void increment() {
ai.incrementAndGet();
}
public static void decrement() {
ai.getAndDecrement();
}
public static void compare() {
ai.compareAndSet(10, 11);
}
public static void main(String []args) {
increment();
decrement();
compare();
check();
System.out.println(ai);
}
}
The program is invoked as follows: java -ea AtomicIntegerTest
What is the expected output of this program? It prints 11 It prints 10 It prints 9 It crashes throwing an AssertionError
Which one of the following options correctly makes use of Callable that will compile without any errors? import java.util.concurrent.Callable;
class CallableTask implements Callable {
public int call() {
System.out.println("In Callable.call()");
return 0;
}
}
import java.util.concurrent.Callable;
class CallableTask extends Callable {
public Integer call() {
System.out.println("In Callable.call()");
return 0;
}
}
import java.util.concurrent.Callable;
class CallableTask implements Callable<Integer> {
public Integer call() {
System.out.println("In Callable.call()");
return 0;
}
}
import java.util.concurrent.Callable;
class CallableTask implements Callable<Integer> {
public void call(Integer i) {
System.out.println("In Callable.call(i)");
}
}
Choose the correct option based on this program: import java.util.concurrent.*;
import java.util.*;
class COWArrayListTest {
public static void main(String []args) {
ArrayList<Integer> aList =
new CopyOnWriteArrayList<Integer>(); // LINE A
aList.addAll(Arrays.asList(10, 20, 30, 40));
System.out.println(aList);
}
}
When executed the program prints the following: [10, 20, 30, 40]. When executed the program prints the following: CopyOnWriteArrayList.class
. The program does not compile and results in a compiler error in line marked with comment LINE A
. When executed the program throws a runtime exception ConcurrentModificationException
. When executed the program throws a runtime exception InvalidOperationException
. Which one of the following methods return a Future object? The overloaded replace()
methods declared in the ConcurrentMap
interface The newThread()
method declared in the ThreadFactory
interface The overloaded submit()
methods declared in the ExecutorService
interface The call()
method declared in the Callable
interface
答案:
D. class PingPong extends Thread {
public void run() {
System.out.println("ping");
}
public static void main(String []args) throws InterruptedException{
Thread pingPong = new PingPong();
pingPong.start();
pingPong.join();
System.out.println("pong");
}
}
The main
thread creates the worker thread and waits for it to complete (which prints “ping”). After that it prints “pong”. So, this implementation correctly prints “ping pong”. Why are the other options wrong? Option a) The main()
method creates the worker thread, but doesn’t start it. So, the code given in this option only prints “pong”. Option b) The program always prints “ping pong”, but it is misleading. The code in this option directly calls the run()
method instead of calling the start()
method. So, this is a single threaded program: both “ping” and “pong” are printed from the main thread. Option c) The main
thread and the worker
thread execute independently without any coordination. (Note that it does not have a call to join()
in the main
method.) So, depending on which thread is scheduled first, you can get “ping pong” or “pong ping” printed. B. Starvation The situation in which low-priority threads keep waiting for a long time to acquire the lock and execute the code in critical sections is known as starvation. A. and B. Abstract methods (in abstract classes or interfaces) cannot be declared synchronized
, hence the options C and D are incorrect. D. It crashes throwing an AssertionError
. The initial value of AtomicInteger
is 10. Its value is incremented by 1 after calling incrementAndGet()
. After that, its value is decremented by 1 after calling getAndDecrement()
. The method compareAndSet
(10, 11) checks if the current value is 10, and if so sets the atomic integer variable to value 11. Since the assert statement checks if the atomic integer value % 2
is zero (that is, checks if it is an even number), the assert fails and the program results in an AssertionError
. C. import java.util.concurrent.Callable;
class CallableTask implements Callable<Integer> {
public Integer call() {
System.out.println("In Callable.call()");
return 0;
}
}
The Callable
interface is defined as follows: public interface Callable<V> {
V call() throws Exception;
}
In option A), the call()
method has the return type int
, which is incompatible with the return type expected for overriding the call
method and so will not compile. In option B), the extends
keyword is used, which will result in a compiler (since Callable
is an interface, the implements
keyword should be used). Option C) correctly defines the Callable
interface providing the type parameter <Integer
>. The same type parameter Integer
is also used in the return type of the call()
method that takes no arguments, so it will compile without errors. In option D), the return type of call()
is void
and the call()
method also takes a parameter of type Integer
. Hence, the method declared in the interface Integer call()
remains unimplemented in the CallableTask
class, so the program will not compile. C. The program does not compile and results in a compiler error in the line marked with comment LINE A. The class CopyOnWriteArrayList
does not inherit from ArrayList
, so an attempt to assign a CopyOnWriteArrayList
to an ArrayList
reference will result in a compiler error. Note that the ArrayList
suffix in the class named CopyOnWriteArrayList
could be misleading as these two classes do not share an IS-A relationship. C. The overloaded submit()
methods declared in ExecutorService
interface Option A) The overloaded replace()
methods declared in the ConcurrentMap
interface remove an element from the map and return the success status (a Boolean value) or the removed value. Option B) The newThread()
is the only method declared in the ThreadFactory
interface and it returns a Thread
object as the return value. Option C) The ExecutorService
interface has overloaded submit()
method that takes a task for execution and returns a Future representing the pending results of the task. Option D) The call()
method declared in Callable
interface returns the result of the task it executed.
十二、使用 JDBC 构建数据库应用
认证目标 |
---|
描述构成 JDBC API 核心的接口,包括驱动程序、连接、语句和结果集接口,以及它们与提供者实现的关系 |
确定使用 DriverManager 类连接到数据库所需的组件,包括 JDBC URL |
提交查询并从数据库中读取结果,包括创建语句、返回结果集、遍历结果以及正确关闭结果集、语句和连接 |
JDBC (Java 数据库连接)是一个重要的 Java API,它定义了客户端如何访问数据库。因此,在构建大规模企业 Java 解决方案时,这一点至关重要。
概括地说,与数据库交互包括以下步骤:
Establish a connection to a database. Execute SQL queries to retrieve, create, or modify tables in the database. Close the connection to the database.
Java 提供了一组 API(JDBC)来执行数据库的这些活动。您可以使用 JDBC 建立与数据库的连接,执行 SQL 查询,并关闭与数据库的连接。JDBC 的好处是你不用为一个特定的数据库写程序。JDBC 在 Java 程序和使用的数据库类型之间创建了一个松散的耦合。例如,数据库建立连接的方式可能不同(API 名称可能不同,等等)。JDBC 隐藏了这些数据库的所有异构性,并提供了一组可以用来与所有类型的数据库进行交互的 API。注意,JDBC 只支持关系数据库,比如 MySQL、Oracle、Microsoft SQL 和 DB2。它不支持 MongoDB 和 Neo4j 等新一代数据库(也称为 NoSQL 数据库)。
从 OCPJP 8 考试的角度来看,你应该知道如何使用 JDBC 连接到数据库,并执行数据库操作,如插入、更新和创建数据库实体。您还应该知道如何提交查询和从数据库中读取结果,以及如何正确地释放数据库资源。
JDBC 类和接口是包java.sql.*
和javax.sql.*
的一部分。本章假设您已经熟悉 SQL 查询,并且对数据库概念有一些基本的了解。本章介绍了 JDBC 4.2,它是 Java SE 8 版本的一部分。
JDBC 简介
认证目标 | |
---|---|
描述构成 JDBC API 核心的接口,包括驱动程序、连接、语句和结果集接口,以及它们与提供者实现的关系 |
让我们看看 JDBC 的重要组件,以及这些组件如何协同工作来实现与数据库的无缝集成。JDBC 的简化架构如图 12-1 所示。Java 应用使用 JDBC API 与数据库进行交互。JDBC API 与 JDBC 驱动程序管理器交互,透明地连接不同类型的数据库并执行各种数据库活动。JDBC 驱动程序管理器使用各种 JDBC 驱动程序连接到它们特定的 DBMSs。
JDBC 车手和车手经理在实现 JDBC 目标的过程中发挥着关键作用。JDBC 驱动程序是专门为与各自的 DBMSs 交互而设计的。驱动程序管理器是 JDBC 驱动程序的目录,它维护着可用数据源及其驱动程序的列表。驱动程序管理器选择合适的驱动程序与各自的 DBMS 通信。它可以管理连接到各自数据源的多个并发驱动程序。
从图中可以看出,异构交互的复杂性被委托给了 JDBC 驱动程序管理器和 JDBC 驱动程序。JDBC API 对应用开发人员隐藏了底层细节和相关的复杂性。
图 12-1。
JDBC architecture
设置数据库
在开始探索 JDBC API 及其用法之前,您必须设置一个要使用的数据库。在开始编写 JDBC 程序之前,需要正确配置数据库。你可以使用任何数据库。本章中的例子使用 MySQL 来解释 JDBC API 的各个方面,因为这个数据库是免费的并且可以广泛获得。本节展示了在您的机器上设置 MySQL 数据库的步骤,假设您使用的是 Windows(如果您使用的是不同的操作系统,步骤会略有不同):
Download the latest MySQL installer from the MySQL download page ( www.mysql.com/downloads/mysql
). Invoke the MySQL installer, and follow all the steps shown by the installation wizard. Keep the default values, and complete the installation. The installer asks you to provide a root/admin password; remember it, because it’s used in the examples. Invoke the MySQL command-line client (in our case, it is MySQL 5.5 Command Line Client, shown on the Start menu). You see a MySQL prompt once you provide the root/admin password.
以下代码建立了一个数据库并创建了两条记录:
Enter password: ********
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.5.27 MySQL Community Server (GPL)
Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> /* Let's create a database for our use.*/
mysql> create database addressBook;
Query OK, 1 row affected (0.01 sec)
mysql> /* Now, let's create a table in this database and insert two records for our use later. */
mysql> use addressBook;
Database changed
mysql> create table contact (id int not null auto_increment, firstName varchar(30) Not null, lastName varchar(30), email varchar(30), phoneNo varchar(13), primary key (id));
Query OK, 0 rows affected (0.20 sec)
mysql> insert into contact values (default, 'Michael', 'Taylor', 'michael@abc.com', '+919876543210');
Query OK, 1 row affected (0.10 sec)
mysql> insert into contact values (default, 'William', 'Becker', 'william@abc.com', '+449876543210');
Query OK, 1 row affected (0.03 sec)
mysql> select * from contact;
+----+-----------+----------+-----------------+---------------+
| id | firstName | lastName | email | phoneNo |
+----+-----------+----------+-----------------+---------------+
| 1 | Michael | Taylor | michael@abc.com | +919876543210 |
| 2 | William | Becker | william@abc.com | +449876543210 |
+----+-----------+----------+-----------------+---------------+
2 rows in set (0.00 sec)
mysql> /* That's it. Our database is ready to use now.*/
连接到数据库
本节讨论如何以编程方式连接到数据库。首先简单介绍一下Connection
界面。
连接接口
java.sql
包的Connection
接口表示从应用到数据库的连接。它是应用和数据库通信的通道。表 12-1 列出了Connection
界面中的重要方法。所有这些方法都抛出SQLException
s,所以在表中没有提到。
表 12-1。
Important Methods in the Connection Interface
方法 | 描述 |
---|---|
Statement createStatement() |
创建一个可用于向数据库发送 SQL 语句的Statement 对象。 |
PreparedStatement prepareStatement(String sql) |
创建一个可以包含 SQL 语句的PreparedStatement 对象。SQL 语句可以有 IN 参数;它们可能包含? 符号,这些符号用作占位符,以便稍后传递实际值。 |
CallableStatement prepareCall(String sql) |
创建一个用于调用数据库中存储过程的CallableStatement 对象。SQL 语句可以有IN 或OUT 参数;它们可能包含? 符号,这些符号用作占位符,用于稍后传递实际值。 |
DatabaseMetaData getMetaData() |
获取DataBaseMetaData 对象。这些元数据包含数据库模式信息、表信息等,当您不知道底层数据库时,这尤其有用。 |
Clob createClob() |
返回一个Clob 对象(Clob 是接口的名称)。字符大对象(CLOB)是 SQL 中的内置类型;它可以用来在数据库表的行中存储列值。 |
Blob createBlob() |
返回一个Blob 对象(Blob 是接口的名称)。二进制大型对象(BLOB)是 SQL 中的内置类型;它可以用来在数据库表的行中存储列值。 |
void setSchema(String schema) |
当传递模式名时,将这个Connection 对象设置为要访问的数据库模式。 |
String getSchema() |
返回与此Connection 对象关联的数据库的架构名称;如果没有与之相关联的模式,则返回null 。 |
使用 DriverManager 连接到数据库
认证目标 | |
---|---|
确定使用 DriverManager 类连接到数据库所需的组件,包括 JDBC URL |
与数据库通信的第一步是在应用和数据库服务器之间建立连接。建立连接需要了解数据库 URL,所以我们现在就来讨论一下。
以下是 JDBC 网址的一般格式:
jdbc:<subprotocol>:<subname>
URL 字符串的一个例子是jdbc:mysql://localhost:3306/
:
- 所有数据库管理系统都一样。
<subprotocol>
因每个 DBMS 而异,在本例中为mysql
。有时它包括供应商名称(在本例中不存在)。<subname>
的格式取决于数据库,但是它的一般格式是//<server>:<port>/database. <server>
取决于您托管数据库的位置。每个 DBMS 使用一个特定的<port>
号(在 MySQL 中是 3306)。最后,提供数据库名称
这里还有几个例子:
jdbc:postgresql://localhost/test
jdbc:oracle://127.0.0.1:44000/test
jdbc:microsoft:sqlserver://himalaya:1433
现在,让我们编写一个简单的应用来获取连接(参见清单 12-1 )。
Listing 12-1. DbConnect.java
import java.sql.Connection;
import java.sql.DriverManager;
// The class attempts to acquire a connection with the database
class DbConnect {
public static void main(String[] args) {
// URL points to JDBC protocol: mysql subprotocol;
// localhost is the address of the server where we installed our
// DBMS (i.e. on local machine) and 3306 is the port on which
// we need to contact our DBMS
String url = "jdbc:mysql://localhost:3306/";
// we are connecting to the addressBook database we created earlier
String database = "addressBook";
// we login as "root" user with password "mysql123"
String userName = "root";
String password = "mysql123";
try (Connection connection = DriverManager.getConnection
(url + database, userName, password)){
System.out.println("Database connection: Successful");
} catch (Exception e) {
System.out.println("Database connection: Failed");
e.printStackTrace();
}
}
}
让我们一步一步地分析程序:
The URL jdbc:mysql://localhost:3306/
indicates that jdbc
is the protocol and mysql
is a subprotocol; localhost
is the address of the server where we installed our DBMS (the local machine), and 3306
is the port on which to contact the DBMS. (Note that this port number is different when you use some other database. We used the default port number provided by the MySQL database, which can be changed if required. Additionally, if you are using another database, the subprotocol also changes.) You need to use the addressBook
database with root
credentials. You can get a connection object by invoking the DriverManager.getConnection()
method. The method expects the URL of the database along with a database name, username, and password. You need to close the connection before exiting the program. This example uses a try-with-resources
statement; hence the close()
method for the connection
is automatically called. If anything goes wrong, you get an exception. In that case, the program prints the exception’s stack trace.
继续运行程序。以下是输出:
Database connection: Failed
java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/addressBook
at java.sql.DriverManager.getConnection(DriverManager.java:604)
at java.sql.DriverManager.getConnection(DriverManager.java:221)
at DbConnect.main(DbConnect.java:16)
哎呀!为什么会有这个SQLException
?当您尝试使用 JDBC 连接到数据库时,DriverManager
会搜索 MySQL 驱动程序。你需要明确地安装相关的驱动程序——它们不是 JDK 的一部分。
您可以从 MySQL 的下载页面( http://dev.mysql.com/downloads/connector/j
)下载连接器。不要忘记将连接器的路径添加到CLASSPATH
中。如果连接器名称是mysql-connector-java-5.1.21-bin.jar
,存储在C:\mysql-connector-java-5.1.21
中,那么将c:\ mysql-connector-java-5.1.21\mysql-connector-java-5.1.21-bin.jar
添加到CLASSPATH
中。
忘记在CLASSPATH
环境变量中添加 jar 的路径是一个常见的错误。在这种情况下,JDBC API 将无法定位 JDBC 驱动程序,并将抛出一个异常。记住,输入 jar 的路径是不够的:在调用 JVM 时,您需要添加 jar 名称以及到CLASSPATH
变量的完整路径,或者用–cp
命令传递 jar 文件的路径。
更新CLASSPATH
变量,然后重试。您可能会得到另一个异常:
Database connection: Failed
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1074)
[... rest of the stack trace elided ...]
这个程序给出了用户名“root”和密码“mysql123”。如果您已经将 root 用户密码设置为其他密码,您将会得到这个异常,并显示消息“拒绝用户访问”有两种方法可以解决这个问题。第一种方法是改变程序给出你的密码,而不是这个程序用的“mysql123”。第二种方法是重置数据库中的密码。对于 MySQL,您可以为用户“root”重置密码,如下所示:
UPDATE mysql.user SET Password=PASSWORD('mysql123') WHERE User='root';
FLUSH PRIVILEGES;
以下是程序成功运行时的输出:
Database connection: Successful
当您看到这个输出时,这意味着您能够与数据库建立连接。如果你想尝试本章余下的程序,你应该让这个程序在你的系统中工作;您需要建立一个连接来查询或更新数据库。
你已经看到了两个从 JDBC API 抛出的
SQLException
的例子。当你得到一个SQLException
时,你很少能在程序中做任何事情来恢复它。在真实的应用中,您可以将其包装为更高级别的异常,并将其重新抛出给调用组件。为了节省本章代码段的空间,我们打印异常的堆栈跟踪,并在程序中忽略它。
了解驱动程序管理器类
DriverManager
类帮助建立程序(用户)和 JDBC 驱动程序之间的连接。该类还跟踪不同的数据源和 JDBC 驱动程序。因此,不需要显式加载 JDBC 驱动程序:DriverManager
搜索合适的驱动程序,如果找到,当您调用getConnection()
方法时自动加载它。清单 12-1 包含以下代码,用于在不显式加载 JDBC 驱动程序时获取连接(在try-with-resources
语句中给出):
Connection connection = DriverManager.getConnection(url + database, userName, password);
驱动程序管理器还管理连接到各自数据源的多个并发驱动程序。表 12-2 列出了DriverManager
类中提供的其他重要方法,包括getConnection()
的重载版本。
表 12-2。
Important Methods in the DriverManager
Class
方法 | 描述 |
---|---|
static Connection getConnection(String url)``static Connection getConnection(String url, Properties info) |
给定数据库 URL,尝试建立连接。此外,您可以直接作为String 参数或通过Properties 文件提供用户名和密码等信息。如果不能建立连接,这个方法抛出一个SQLException 。 |
static Driver getDriver(String url) |
搜索已注册的 JDBC 驱动程序列表,如果找到,返回与数据库 URL 匹配的适当的Driver 对象。 |
static void registerDriver(Driver driver) |
添加到DriverManager 中已注册的Driver 对象列表。 |
static void deregisterDriver(Driver driver) |
从DriverManager 中已注册的Driver 对象列表中注销驱动程序 |
使用getDriver()
方法,您可以通过传递数据库 URL 来加载驱动程序:
String url = "jdbc:mysql://localhost:3306/";
Driver driver = DriverManager.getDriver(url);
System.out.println(driver.getClass().getName());
这段代码打印了com.mysql.jdbc.Driver
—这是 MySQL JDBC 驱动程序的完全限定名,并且DriverManager
能够加载它。从这个Driver
对象,您可以通过调用connect()
方法并传递数据库 URL 和可选的Properties
文件引用来建立连接:
Connection connection = driver.connect(url, /*properties = */ null);
在Properties
文件中,您可以提供用户名和密码以及任何其他详细信息。
查询和更新数据库
认证目标 | |
---|---|
提交查询并从数据库中读取结果,包括创建语句、返回结果集、遍历结果以及正确关闭结果集、语句和连接 |
一旦建立了到所需数据库的连接,就可以对它执行各种操作。常见的操作被缩写为 CRUD(创建、读取、更新、删除)。您可以使用SELECT
SQL 语句读取数据,并使用INSERT
、UPDATE
和DELETE
修改数据库。JDBC 提供了两个支持查询的重要接口:Statement
和ResultSet
。接下来的两个小节将讨论这些接口。
语句接口
顾名思义,Statement
是一个 SQL 语句,可以用来将 SQL 语句与连接的数据库进行通信,并从数据库接收结果。您可以使用Statement
形成 SQL 查询,并使用Statement
接口(或其派生接口之一)中提供的 API 来执行它。Statement
有三种口味:Statement
、PreparedStatement
、CallableStatement
;这些显示在图 12-2 中的继承层次中。
图 12-2。
The Statement
interface and its subinterfaces
对于给定的情况,您如何从这三个Statement
界面中进行选择?让我们来看看不同之处:
Statement
:向数据库发送不带任何参数的 SQL 语句。对于典型的用途,您需要使用这个接口。您可以在Connection
接口中使用createStatement()
方法创建一个Statement
的实例。PreparedStatement
:表示可以使用IN
参数定制的预编译 SQL 语句。通常,它比一个Statement
对象更有效率;因此,它用于提高性能,尤其是当一条 SQL 语句被执行多次时。通过调用Connection
接口中的preparedStatement()
方法,可以得到PreparedStatement
的一个实例。CallableStatement
:执行存储过程。CallableStatement
实例可以处理IN
以及OUT
和INOUT
参数。您需要调用Connection
接口中的prepareCall()
方法来获取该类的一个实例。
一旦创建了适当的Statement
对象,就可以执行 SQL 语句了。Statement
接口提供了三种执行方法:executeQuery()
、executeUpdate()
和execute()
。如果您的 SQL 语句是一个SELECT
查询,使用executeQuery()
方法,它返回一个ResultSet
(将在下一节讨论)。当您想要使用INSERT
、UPDATE
或DELETE
语句更新数据库时,您应该使用executeUpdate()
方法,该方法返回一个反映更新的行数的整数。如果不知道 SQL 语句的类型,可以使用execute()
方法,它可能返回多个结果集或多个更新计数,或者两者的组合。从 OCPJP 8 考试的角度,你需要了解Statement
接口及其派生接口(见表 12-3 )。本章的其余部分使用Statement
接口。
表 12-3。
Important Methods of the Statement
Interface
方法 | 描述 |
---|---|
boolean execute(String sql) |
执行给定的 SQL 查询。如果查询产生了一个ResultSet ,这个方法返回 true。您可以通过调用getResultSet() 方法来检索ResultSet 对象。如果 SQL 查询没有结果或者有更新计数,则此方法返回 false。您可以使用getUpdateCount() 方法来获取更新计数。在极少数情况下,该方法可能会返回多个ResultSets ;在这种情况下,可以调用getMoreResults() 方法。 |
ResultSet executeQuery(String sql) |
执行查询并返回ResultSet 对象作为结果。如果没有结果,该方法不返回null ;相反,当调用next() 方法时,返回的ResultSet 对象将返回 false。 |
int executeUpdate(String sql) |
执行CREATE 、INSERT 、UPDATE 或DELETE SQL 查询。它返回更新的行数(如果没有结果,则返回零,比如使用CREATE 语句)。 |
Connection getConnection() |
返回用来创建Statement 对象的Connection 对象。 |
void close() |
关闭与此Statement 对象相关联的数据库和其他 JDBC 资源。在已经关闭的Statement 对象上调用close() 没有任何效果。 |
根据 SQL 语句的类型选择相关的
execute
方法。记住每个execute
方法返回不同的输出。方法executeQuery()
返回一个ResultSet
,executeUpdate()
返回一个更新计数,execute()
方法可能返回多个ResultSet
或多个更新计数,或者两者的组合。
结果集接口
关系数据库包含表。每个表都有一组由列表示的属性(由表建模的对象的属性);行是包含这些属性值的记录。查询数据库时,会产生表格数据:包含查询所请求的列的一定数量的行。这种表格数据被称为结果集。结果集是一个包含列标题和查询所请求的相关值的表。
A
resultset 维护一个指向当前行的光标。您一次只能读取一行,因此您必须更改光标的位置来读取/导航整个结果集。最初,光标被设置在第一行之前。您需要在 resultset 上调用next()
方法,将光标位置向前移动一行。此方法返回一个布尔值;因此,您可以在一个while
循环中使用它来迭代整个结果集。表 12-4 显示了ResultSet
支持的其他移动光标的方法。
表 12-4。
Useful ResultSet
Methods to Move the Cursor
方法 | 描述 |
---|---|
void beforeFirst() |
将光标设置在结果集的第一行之前。 |
void afterLast() |
将光标设置在结果集的最后一行之后。 |
boolean absolute(int rowNumber) |
将光标设置到请求的行号(表中的绝对位置,而不是相对于当前位置)。 |
boolean relative(int rowNumber) |
将光标设置到相对于当前位置的所请求的行号。rowNumber 可以是正值,也可以是负值:正值相对于当前位置向前移动,负值相对于当前位置向后移动。 |
boolean next() |
将光标设置到结果集的下一行。 |
boolean previous() |
将光标设置到结果集的前一行。 |
图 12-3 举例说明了这些方法。该图有 5 行,光标指向 ID 为 3 的行。如果调用beforeFirst()
,光标会移动到行 ID 1 之前的位置。如果调用afterLast()
,光标会移动到行 ID 5 之后的位置。如果您调用relative(-2)
,因为当前位置在第 3 行,光标向后移动两个位置,指向第 1 行的位置。调用previous()
和next()
将光标分别移动到行 ID 为 2 和行 ID 为 4 的位置。最后,调用absolute(5)
将光标移动到行 ID 为 5 的位置。
图 12-3。
Moving the cursor by calling ResultSet
methods
ResultSet
还提供了一组方法来读取当前行中所需列的值。通常,这些方法有两种风格:第一种风格接受列号作为输入,第二种风格接受列名作为输入。例如,读取double
值的方法有double getDouble(int columnNumber)
和double getDouble(String columnName)
。同样,ResultSet
为所有基本类型提供了get()
方法。
类似地,ResultSet
提供了一组方法来更新所选行中所需列的值。这些方法也有两种变体:void updateXXX(int columnNumber, XXX x)
和void updateXXX(String columnName, XXX x)
,其中update
方法是为表示为XXX
的各种数据类型定义的。
查询数据库
现在,您已经知道了用于在数据库上执行简单 SQL 查询的所有必要接口:Connection
、Statement
和ResultSet
。图 12-4 显示了建立数据库连接、执行 SQL 查询和处理结果的高级步骤。
图 12-4。
Connecting to, querying, and processing results from a database
让我们查询一个数据库并打印输出。回想一下,您在这个数据库中创建了一个名为addressBook
的数据库和一个名为contact
的表,并在表中插入了两行。假设您想要打印表格内容;清单 12-2 和 12-3 包含这样做的程序
Listing 12-2. DbConnector.java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
// Utility class with method connectToDb() that will be used by other programs in this chapter
public class DbConnector {
public static Connection connectToDb() throws SQLException {
String url = "jdbc:mysql://localhost:3306/";
String database = "addressBook";
String userName = "root";
String password = "mysql123";
return DriverManager.getConnection(url + database, userName, password);
}
}
Listing 12-3. DbQuery
.java
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
// Program to illustrate how to query a database
class DbQuery {
public static void main(String[] args) {
// Get connection, execute query, get the result set
// and print the entries from the result rest
try (Connection connection = DbConnector.connectToDb();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM contact")){
System.out.println("ID \tfName \tlName \temail \t\tphoneNo");
while (resultSet.next()) {
System.out.println(resultSet.getInt("id") + "\t"
+ resultSet.getString("firstName") + "\t"
+ resultSet.getString("lastName") + "\t"
+ resultSet.getString("email") + "\t"
+ resultSet.getString("phoneNo"));
}
}
catch (SQLException sqle) {
sqle.printStackTrace();
System.exit(-1);
}
}
}
该程序的输出如下:
ID fName lName email phoneNo
1 Michael Taylor michael@abc.com +919876543210
2 William Becker william@abc.com +449876543210
让我们一步一步地看看这段代码中发生了什么:
- 在
main()
方法中,有一个try-with-resources
语句。第一条语句是对程序中定义的connectToDb()
方法的调用。connectToDb()
方法只是连接到数据库(在上一个例子中可以看到),如果成功就返回一个Connection
对象。 - 下一条语句从
connection
创建一个Statement
对象。 Statement
对象现在用于执行一个查询。您想要获取contact
表中的所有列;因此,您将SELECT * FROM contact
写成一个 SQL 查询。使用Statement
对象的executeQuery()
方法执行查询。查询的结果存储在一个ResultSet
对象中。ResultSet
对象用于打印获取的数据。您读取当前行中的所有列值,并对ResultSet
对象中的每一行执行同样的操作。- 因为您已经在一个
try-with-resources
语句中创建了Connection
、Statement
和ResultSet
对象,所以没有必要在这些资源上显式调用close()
。然而,如果您没有使用try-with-resources
,您需要在finally
块中显式地释放它们。
这里,您使用列名来读取相关的值。您可以使用列号来做同样的工作。下面是在while
循环中使用列号的修改代码:
while (resultSet.next()) {
System.out.println(resultSet.getInt(1)
+ "\t" + resultSet.getString(2)
+ "\t" + resultSet.getString(3)
+ "\t" + resultSet.getString(4)
+ "\t" + resultSet.getString(5));
}
这段代码产生的结果与上一个示例完全相同。然而,重要的是要注意这里的列索引从 1 开始,而不是从 0 开始。
ResultSet
对象中的列索引从 1 开始,而不是从 0 开始。
当按列索引引用列时,如果按大于总列数的索引引用列,则会出现异常。例如,如果您将上一个示例中使用的列索引之一更改为 6,则会出现以下异常:
java.sql.SQLException: Column Index out of range, 6 > 5.
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1074)
[... this part of the stack trace elided ...]
at DbQuery.main(DbQuery.java:18)
请务必提供正确的列索引。
在本例中,您知道列数以及列中的数据类型。如果您既不知道每行的列数,也不知道列中的数据类型,该怎么办?在这种情况下,首先需要在返回一个ResultMetaData
对象的ResultSet
对象上调用getMetaData()
方法;在ResultMetaData
对象上,您可以使用getColumnCount()
方法来获取列计数。当您不知道列条目的数据类型时,您可以在ResultSet
对象上使用getObject()
方法。您可以将列索引传递给getObject()
来获取相应列中的值。下面是使用这些方法的修改后的代码:
// from resultSet metadata, find out how many columns there are
// and then read the column entries
int numOfColumns = resultSet.getMetaData().getColumnCount();
while (resultSet.next()) {
// remember that the column index starts from 1 not 0
for(int i = 1; i <= numOfColumns; i++) {
// since we do not know the data type of the column, we use getObject()
System.out.print(resultSet.getObject(i) + "\t");
}
System.out.println("");
}
该程序的输出是
ID fName lName email phoneNo
1 Michael Taylor michael@abc.com +919876543210
2 William Becker william@abc.com +449876543210
让我们做另一个练习。这一次,您只想打印名字与“Michael”匹配的记录的名字和电子邮件地址(参见清单 12-4 )。
Listing 12-4. DbQuery4.java
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
class DbQuery4 {
public static void main(String[] args) throws SQLException {
try (Connection connection = DbConnector.connectToDb();
Statement statement = connection.createStatement();
ResultSet resultset = statement.executeQuery("SELECT firstName, email FROM contact WHERE firstName=\"Michael\"")) {
System.out.println("fName \temail");
while (resultset.next()){
System.out.println(resultset.getString("firstName") + "\t"
+ resultset.getString("email"));
}
} catch (SQLException e) {
e.printStackTrace();
System.exit(-1);
}
}
}
该程序打印以下内容:
fName email
Michael michael@abc.com
更新数据库
现在让我们更新数据库。有两种方法可以做到这一点:可以使用 SQL 查询直接更新数据库,或者可以使用 SQL 查询获取结果集,然后更改它和数据库。JDBC 支持这两种方法。让我们专注于检索结果集并修改它和数据库。
为了修改结果集和数据库,ResultSet
类为每种数据类型提供了一组更新方法。还有其他的支持方法,比如updateRow()
和deleteRow()
,让任务更简单。是时候动手了:假设您的addressBook
数据库中的一个联系人更改了他们的电话号码,您需要使用 JDBC 程序更新数据库中的电话号码(参见清单 12-5 )。
Listing 12-5. DbUpdate.java
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
// To illustrate how we can update a database
class DbUpdate {
public static void main(String[] args) throws SQLException {
try (Connection connection = DbConnector.connectToDb();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM contact WHERE firstName=\"Michael\"")) {
// first fetch the data and display it before the update operation
System.out.println("Before the update");
System.out.println("id \tfName \tlName \temail \t\tphoneNo");
while (resultSet.next()) {
System.out.println(resultSet.getInt("id") + "\t"
+ resultSet.getString("firstName") + "\t"
+ resultSet.getString("lastName") + "\t"
+ resultSet.getString("email") + "\t"
+ resultSet.getString("phoneNo"));
}
// now update the resultset and display the modified data
resultSet.absolute(1);
resultSet.updateString("phoneNo", "+919976543210");
System.out.println("After the update");
System.out.println("id \tfName \tlName \temail \t\tphoneNo");
resultSet.beforeFirst();
while (resultSet.next()) {
System.out.println(resultSet.getInt("id") + "\t"
+ resultSet.getString("firstName") + "\t"
+ resultSet.getString("lastName") + "\t"
+ resultSet.getString("email") + "\t"
+ resultSet.getString("phoneNo"));
}
} catch (SQLException e) {
e.printStackTrace();
System.exit(-1);
}
}
}
让我们一步一步地找出程序的本质:
- 您使用
DbConnector.connectToDb()
方法建立连接。 - 在创建了一个
Statement
对象之后,您在数据库上执行一个查询来查找与 Michael 相关联的记录。(为了简单起见,假设结果集将只包含一条记录。) - 您打印检索到的记录。
- 使用
absolute()
方法将光标移动到ResultSet
对象的第一行;然后你用updateString(
方法更新电话号码。 - 最后,打印修改后的结果集。
这看起来很简单。执行程序并查看它打印的内容:
Before the update
id fName lName email phoneNo
1 Michael Taylor michael@abc.com +919876543210
com.mysql.jdbc.NotUpdatable: Result Set not updatable.(...rest of the text elided)
at com.mysql.jdbc.ResultSetImpl.updateString(ResultSetImpl.java:8618)
at com.mysql.jdbc.ResultSetImpl.updateString(ResultSetImpl.java:8636)
at DbUpdate.main(DbUpdate.java:34)
糟糕——程序在抛出异常后崩溃了!发生了什么事?
您正试图更新不可更新的ResultSet
对象。为了在结果集和数据库中进行更新,您需要使这个结果集可更新。您可以通过创建一个合适的Statement
对象来实现这一点;在调用createStatement()
方法时,您可以传递输入,比如您是想要对变化敏感的可滚动结果集还是可更新结果集。
对清单 12-5 中对createStatement()
的调用做如下修改:
Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
现在运行更改后的程序,看看它是否工作:
Before the update
id fName lName email phoneNo
1 Michael Taylor michael@abc.com +919876543210
After the update
id fName lName email phoneNo
1 Michael Taylor michael@abc.com +919876543210
很好,程序没有导致异常。但是请等待—电话号码没有更新!发生了什么事?更新后你忘了一个至关重要的声明:updateRow()
方法。每次使用适当的updateXXX()
方法对结果集进行更改时,都需要调用updateRow()
来确保数据库中的所有值都得到更新。做出这个改变,然后再试一次(见清单 12-6 )。
Listing 12-6. DbUpdate2.java
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
// To illustrate how we can update a database
class DbUpdate2 {
public static void main(String[] args) throws SQLException {
try (Connection connection = DbConnector.connectToDb();
// create a statement from which the created ResultSets
// are "scroll sensitive" as well as "updatable"
Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet resultSet = statement.executeQuery("SELECT * FROM contact WHERE firstName=\"Michael\"")) {
// first fetch the data and display it before the update operation
System.out.println("Before the update");
System.out.println("id \tfName \tlName \temail \t\tphoneNo");
while (resultSet.next()) {
System.out.println(resultSet.getInt("id") + "\t"
+ resultSet.getString("firstName") + "\t"
+ resultSet.getString("lastName") + "\t"
+ resultSet.getString("email") + "\t"
+ resultSet.getString("phoneNo"));
}
// now update the resultset and display the modified data
resultSet.absolute(1);
resultSet.updateString("phoneNo", "+919976543210");
// reflect those changes back to the database
// by calling updateRow() method
resultSet.updateRow();
System.out.println("After the update");
System.out.println("id \tfName \tlName \temail \t\tphoneNo");
resultSet.beforeFirst();
while (resultSet.next()) {
System.out.println(resultSet.getInt("id") + "\t"
+ resultSet.getString("firstName") + "\t"
+ resultSet.getString("lastName") + "\t"
+ resultSet.getString("email") + "\t"
+ resultSet.getString("phoneNo"));
}
} catch (SQLException e) {
e.printStackTrace();
System.exit(-1);
}
}
}
修订后的程序打印以下内容:
Before the update
id fName lName email phoneNo
1 Michael Taylor michael@abc.com +919876543210
After the update
id fName lName email phoneNo
1 Michael Taylor michael@abc.com +919976543210
它运行良好。现在您已经知道了更新数据库中的行所需的要求和步骤。
修改行内容后总是调用
updateRow
;否则,您将丢失更改。
接下来,在结果集和数据库中插入一条记录怎么样?尝试清单 12-7 中所示的例子。
Listing 12-7. DbInsert.java
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
// To illustrate how to insert a row in a resultset and in the database
class DbInsert {
public static void main(String[] args) throws SQLException {
try (Connection connection = DbConnector.connectToDb();
Statement statement = connection.createStatement(
ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet resultSet =
statement.executeQuery("SELECT * FROM contact")) {
System.out.println("Before the insert");
System.out.println("id \tfName \tlName \temail \t\tphoneNo");
while (resultSet.next()){
System.out.println(resultSet.getInt("id") + "\t"
+ resultSet.getString("firstName") + "\t"
+ resultSet.getString("lastName") + "\t"
+ resultSet.getString("email") + "\t"
+ resultSet.getString("phoneNo"));
}
resultSet.moveToInsertRow();
resultSet.updateString("firstName", "John");
resultSet.updateString("lastName", "K.");
resultSet.updateString("email", "john@abc.com");
resultSet.updateString("phoneNo", "+19753186420");
resultSet.insertRow();
System.out.println("After the insert");
System.out.println("id \tfName \tlName \temail \t\tphoneNo");
resultSet.beforeFirst();
while (resultSet.next()){
System.out.println(resultSet.getInt("id") + "\t"
+ resultSet.getString("firstName") + "\t"
+ resultSet.getString("lastName") + "\t"
+ resultSet.getString("email") + "\t"
+ resultSet.getString("phoneNo"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
这个例子中发生了什么?打印完当前记录后,调用moveToInsertRow()
方法。该方法将游标设置为新记录,并为插入行准备结果集(它创建一个缓冲区来保存列值)。之后,您使用updateString()
来修改新添加的行中的每个列值。最后,调用insertRow()
将新行插入结果集和数据库。这里需要注意的一件重要事情是,您需要为每一列提供正确类型的值。此外,如果列值不能留空,则不能将列留空(不提供任何值)。在这两种违规情况下,你可能会得到一个SQLException
。
让我们看看这个程序输出了什么:
Before the insert
id fName lName email phoneNo
1 Michael Taylor michael@abc.com +919976543210
2 William Becker william@abc.com +449876543210
After the insert
id fName lName email phoneNo
1 Michael Taylor michael@abc.com +919976543210
2 William Becker william@abc.com +449876543210
3 John K. john@abc.com +19753186420
看起来不错!现在让我们尝试另一个操作:从数据库中删除一条记录。看看清单 12-8 中的程序。
Listing 12-8. DbDelete.java
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
// To illustrate how to delete a row in a resultset and in the database
class DbDelete {
public static void main(String[] args) throws SQLException {
try (Connection connection = DbConnector.connectToDb();
Statement statement = connection.createStatement(
ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet resultSet1 = statement.executeQuery
("SELECT * FROM contact WHERE firstName=\"John\"")) {
if(resultSet1.next()){
// delete the first row
resultSet1.deleteRow();
}
resultSet1.close();
// now fetch again from the database
try (ResultSet resultSet2 = statement.executeQuery("SELECT * FROM contact")) {
System.out.println("After the deletion");
System.out.println("id \tfName \tlName \temail \t\tphoneNo");
while (resultSet2.next()){
System.out.println(resultSet2.getInt("id") + "\t"
+ resultSet2.getString("firstName") + "\t"
+ resultSet2.getString("lastName") + "\t"
+ resultSet2.getString("email") + "\t"
+ resultSet2.getString("phoneNo"));
}
}
} catch (SQLException e) {
e.printStackTrace();
System.exit(-1);
}
}
}
这个程序简单地选择一个合适的行来删除,并在当前选择的行上调用deleteRow()
方法。下面是程序的输出:
After the deletion
id fName lName email phoneNo
1 Michael Taylor michael@abc.com +919976543210
2 William Becker william@abc.com +449876543210
这个程序运行良好,正确地删除了这个人的名字是 John 的行。
您可能记得您在数据库中创建了一个名为contact
的表。当时,您从 MySQL 命令提示符下创建了这个表。使用 JDBC 程序可以完成同样的任务。让我们以编程方式在数据库中创建一个名为familyGroup
的新表(参见清单 12-9 )。
Listing 12-9. DbCreateTable.java
import java.sql.Connection;
import java.sql.Statement;
import java.sql.SQLException;
class DbCreateTable {
public static void main(String[] args) {
try (Connection connection = DbConnector.connectToDb();
Statement statement = connection.createStatement()){
// use CREATE TABLE SQL statement to
// create table familyGroup
statement.executeUpdate("CREATE TABLE familyGroup (id int not null auto_increment, nickName varchar(30) not null, primary key(id));");
System.out.println("Table created successfully");
}
catch (SQLException sqle) {
sqle.printStackTrace();
System.exit(-1);
}
}
}
该程序打印以下内容:
Table created successfully
该计划正在按预期运行。像前面一样,连接到数据库并获取Statement
对象。然后,使用executeUpdate()
方法发出一条 SQL 语句。使用 SQL 语句,您声明需要创建一个名为familyGroup
的表以及两列:id
和nickName
。此外,您声明应该将id
视为主键。就是这样;SQL 语句在数据库中创建一个新表。
当您传递一个有语法错误的 SQL 语句时会发生什么?例如,如果您将“TABLE”拼错为“TABL ”,则会出现以下异常:
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'TABL familyGroup (id int not null auto_increment, nickName varchar(30) not null,' at line 1
传递正确的 SQL 语句(没有语法错误)是您的责任。
要记住的要点
以下是一些可能对你的 OCPJP 八级考试有帮助的要点:
ResultSet
中的boolean absolute(int)
方法将光标移动到那个ResultSet
对象中传递的行号。如果行号为正,则从ResultSet
对象的开头移动到该位置;如果行号为负,则从ResultSet
对象的末尾移动到该位置。假设ResultSet
对象中有十个条目。调用absolute(3
)将光标移动到第三行。调用absolute(
-bs)
将光标移动到第八行。如果给出超出范围的值,光标会移动到开头或结尾。- 在一个
ResultSet
对象中,调用absolute(1)
相当于调用first()
,调用absolute(-1)
相当于调用last()
。 - 您可以在
ResultSet
方法中使用列名或列索引。您使用的索引是ResultSet
对象的索引,而不是数据库表中的列号。 - 如果
Statement
对象被关闭、重新执行或检索下一组结果,则Statement
对象关闭当前的ResultSet
对象。这意味着没有必要用一个ResultSet
对象显式调用close()
;然而,一旦处理完对象,调用close()
是一个很好的做法。 - 您可以使用
ResultSet
对象的列名,而不用担心大小写:getXXX()
方法接受不区分大小写的列名来检索相关的值。 - 考虑这样一种情况,在一个
ResultSet
对象中有两列同名。如何使用列名检索关联的值?如果使用列名来检索值,它总是指向与给定名称匹配的第一列。因此,在这种情况下,您必须使用列索引来检索与这两列相关联的值。 - 您可能还记得,
PreparedStatement
接口继承自Statement
。然而,PreparedStatement
覆盖了所有的execute
方法。例如,executeUpdate()
的行为可能不同于它的基方法。 - 您可以取消使用方法
cancelRowUpdates()
所做的任何更新。但是,必须在调用updateRow()
之前调用这个方法。在所有其他情况下,它对行没有影响。 - 连接到数据库时,您需要指定正确的用户名和密码。如果提供的用户名或密码不正确,您会得到一个
SQLException
。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试前阅读这一部分。
确定使用 DriverManager 类连接到数据库所需的组件,包括 JDBC URL
- JDBC 隐藏了所有 DBMSs 的异构性,并提供了一组 API 来与所有类型的数据库进行交互。异构交互的复杂性被委托给 JDBC 驱动程序管理器和 JDBC 驱动程序。
DriverManager
类中的getConnection()
方法有三个参数:URL 字符串、用户名字符串和密码字符串。- URL 的语法(需要指定它来获得
Connection
对象)是jdbc: <subprotocol>:<subname>
。 - 如果 JDBC API 不能定位 JDBC 驱动程序,它抛出一个
SQLException.
如果驱动程序的 jar 可用,它们需要被包含在类路径中以使 JDBC API 能够定位驱动程序。
描述构成 JDBC API 核心的接口,包括驱动程序、连接、语句和结果集接口,以及它们与提供者实现的关系
java.sql.Connection
接口提供了应用和数据库通信的通道。- JDBC 支持两类查询和更新:
Statement
和Resultset
。 Statement
是一个 SQL 语句,可用于将 SQL 语句传递给连接的数据库,并从数据库接收结果。有三种类型的Statement
:Statement
:向数据库发送不带任何参数的 SQL 语句PreparedStatement
:表示可以使用IN
参数定制的预编译 SQL 语句CallableStatement
:执行存储过程;可以处理IN
以及OUT
和INOUT
参数
- 结果集是一个带有列标题和查询所请求的相关值的表。
提交查询并从数据库中读取结果,包括创建语句、返回结果集、遍历结果以及正确关闭结果集、语句和连接
- 一个
ResultSet
对象保持一个指向当前行的光标。最初,光标被设置在第一行之前;调用next()
方法将光标位置前移一行。 ResultSet
对象中的列索引从 1 开始(不是从 0 开始)。- 修改结果集中的行内容后,需要调用
updateRow()
;否则,对ResultSet
对象所做的更改将会丢失。 - 您可以使用
try-with-resources
语句自动关闭资源(Connection
、ResultSet
和Statement
)。
Question TimeWhich one of the given options is not correct with respect to the driver manager belonging to the JDBC architecture? A driver manager maintains a list of available data sources and their drivers. A driver manager chooses an appropriate driver to communicate to the respective DBMS. A driver manager ensures the atomic properties of a transaction. A driver manager manages multiple concurrent drivers connected to their respective data sources. Consider the following code segment. Assume that the connection object is valid and the statement.executeQuery()
method successfully returns a ResultSet
object with a few rows in it: Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM contact"));
int numOfColumns = resultSet.getMetaData().getColumnCount();
while (resultSet.next()) {
// traverse the columns by index values
for(int i = 0; i < numOfColumns; i++) {
// since we do not know the data type of the column, we use getObject()
System.out.print(resultSet.getObject(i) + "\t");
}
System.out.println("");
}
Which of the following statements is true regarding this code segment? The code segment will successfully print the contents of the rows in the ResultSet
object. The looping header is wrong. To traverse all the columns, it should be for(int i = 0; i <= numOfColumns; i++) {
The looping header is wrong. To traverse all the columns, it should be for(int i = 1; i <= numOfColumns; i++) {
The looping header is wrong. To traverse all the columns, it should be for(int i = 1; i < numOfColumns; i++) {
Consider this program, and choose the best option describing its behavior (assume that the connection is valid): try (Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM contact")){
System.out.println(resultSet.getInt("id") + "\t"
+ resultSet.getString("firstName") + "\t"
+ resultSet.getString("lastName") + "\t"
+ resultSet.getString("email") + "\t"
+ resultSet.getString("phoneNo"));
}
catch (SQLException sqle) {
System.out.println("SQLException");
}
This program will print the following: SQLException
. This program will print the first row from contact
. This program will print all the rows from contact
. This program will report compiler errors. Which two of the following statements are true regarding Statement
and its derived types? Statement
can handle SQL queries with IN
, OUT
, and INOUT
parameters. PreparedStatement
is used to execute stored procedures. You can get an instance of PreparedStatement
by calling the preparedStatement()
method in the Connection
interface. CallableStatement
extends the PreparedStatement
class; PreparedStatement
in turn extends the Statement
class. The Statement
interface and its derived interfaces implement the AutoCloseable
interface, hence Statement
objects can be used with the try-with-resources
statement. Which one of the following statements is a correct way to instantiate a Statement
object? Statement statement = connection.getStatement();
Statement statement = connection.createStatement();
Statement statement = connection.newStatement();
Statement statement = connection.getStatementInstance();
Consider the following code snippet: try(ResultSet resultSet = statement.executeQuery("SELECT * FROM contact")) {
// Stmt #1
resultSet.updateString("firstName", "John");
resultSet.updateString("lastName", "K.");
resultSet.updateString("email", "john@abc.com");
resultSet.updateString("phoneNo", "+19753186420");
// Stmt #2
// rest of the code elided
}
Assume that resultSet
and statement
are legitimate instances of the ResultSet
and Statement
interfaces, respectively. Which one of the following statements is correct with respect to Stmt #1 and Stmt #2 for successfully inserting a new row? Replacing Stmt #1 with resultSet.moveToInsertRow()
will make the program work. Replacing Stmt #1 with resultSet.insertRow()
will make the program work. Replacing Stmt #1 with resultSet.moveToInsertRow()
and Stmt #2 with resultSet.insertRow()
will make the program work. Replacing Stmt #1 with resultSet.insertRow()
and Stmt #2 with resultSet.moveToInsertRow()
will make the program work. Which one of the following statements is true with respect to ResultSet?
Calling absolute(1)
on a ResultSet
instance is equivalent to calling first()
, and calling absolute(-1)
is equivalent to calling last()
. Calling absolute(0)
on a ResultSet
instance is equivalent to calling first()
, and calling absolute(-1)
is equivalent to calling last()
. Calling absolute(-1)
on a ResultSet
instance is equivalent to calling first()
, and calling absolute(0)
is equivalent to calling last()
. Calling absolute(1)
on a ResultSet
instance is equivalent to calling first()
, and calling absolute(0)
is equivalent to calling last()
. Consider the following code snippet. Assume that DbConnector.connectToDb()
returns a valid Connection
object and that the contact
table has an entry with the value “Michael” in the firstName
column: ResultSet resultSet = null;
try (Connection connection = DbConnector.connectToDb()) {
Statement statement = connection.createStatement();
resultSet = statement.executeQuery("SELECT * FROM contact WHERE firstName LIKE 'M%'"); // #LINE1
}
while (resultSet.next()){ //#LINE2
// print the names by calling resultSet.getString("firstName"));
}
This program results in a compiler error in the statement marked with comment #LINE1
. This program results in a compiler error in the statement marked with comment #LINE2
. This program crashes by throwing an SQLException
. This program crashes by throwing a NullPointerException
. This program prints firstName
column values that start with the character “M”.
答案:
C. A driver manager ensures the atomic properties of a transaction. The other three options A, B, and D are true. A driver manager maintains a list of available data sources and their drivers. Given a database URL, the driver manager chooses an appropriate driver to communicate with the respective DBMS. Further, a driver manager manages multiple concurrent drivers connected to their respective data sources. However, it is not responsible for maintaining atomicity properties when performing transactions. C. The looping header is wrong. To traverse all the columns, it should be for(int i = 1; i <= numOfColumns; i++) {
Given N columns in a table, the valid column indexes are from 1 to N, not 0 to N - 1. A. This program will print the following: SQLException
. The statement while (resultSet.next())
is missing. The correct options are C and E. You can get an instance of PreparedStatement
by calling the preparedStatement()
method in the Connection
interface. The Statement
interface and its derived interfaces implement the AutoCloseable
interface, so Statement
objects can be used with the try-with-resources
statement. The other three options A, B, and D are incorrect for the following reasons: A: Statement
objects can be used for SQL queries that have no parameters. Only a CallableStatement
can handle IN
, OUT
, and INOUT
parameters. B: PreparedStatement
is used for precompiled SQL statements. The CallableStatement
type is used for stored procedures. D: CallableStatement
implements the PreparedStatement
interface. PreparedStatement
in turn implements the Statement i
nterface. Further, these three types are interfaces, not classes. B. Statement statement = connection.createStatement();
C. Replacing Stmt #1 with resultSet.moveToInsertRow()
; and Stmt #2 with resultSet.insertRow()
; will make the program work. You need to call the moveToInsertRow()
method in order to insert a new row: this method prepares the resultset for creating a new row. Once the row is updated, you need to call insertRow()
to insert the row into the resultset and the database. A. Calling absolute(1)
on a ResultSet
instance is equivalent to calling first()
, and calling absolute(-1)
is equivalent to calling last()
. C. This program crashes by throwing a SQLException
. The try-with-resources
block is closed before the while
statement executes. Calling resultSet.next()
results in making a call on the closed ResultSet
object. Hence, this program results in throwing a SQLException
(“Operation not allowed after ResultSet closed”).
十三、本地化
认证目标 |
---|
使用 locale 对象读取和设置区域设置 |
创建并读取属性文件 |
为每个语言环境构建一个资源包,并在应用中加载一个资源包 |
今天,计算机和软件变得如此普遍,以至于它们在世界各地都被用于人类活动。对于任何与这些用户相关和有用的软件,它都需要本地化。我们使软件适应当地语言和习俗的过程称为本地化。
本地化就是让软件对来自不同文化的用户有用,换句话说,就是为来自不同国家或语言的用户定制软件。如何本地化一个软件应用?当您本地化软件应用时,应该注意两个重要的准则:
- 不要硬编码文本(比如给用户的消息,GUI 中的文本元素,等等。)并将它们分离到外部文件或专用类中。完成这一步后,在软件中添加对新语言环境的支持就变得轻而易举了。
- 考虑到本地化,处理特定于文化的方面,如日期、时间、货币和格式化数字。不要假定一个默认的区域设置,而是以这样一种方式设计,即获取和定制当前的区域设置。
在这一章中,你将学习如何本地化你的软件。本地化主要包括为不同的地区创建资源包,以及通过使软件适应不同的地区来使软件具有文化意识。在本章中,您还将学习如何创建和使用这些资源包。
现场
认证目标 | |
---|---|
使用 locale 对象读取和设置区域设置 |
场所是“代表一个国家、语言或文化的地方”考虑一下加拿大-法国语言环境。加拿大的许多地方都讲法语,这可能是一个地区。换句话说,如果你想销售为说法语的加拿大人定制的软件,那么你需要使你的软件适合这个地区。在 Java 中,这个地区由代码fr_CA
表示,其中fr
是法语的缩写,CA
是加拿大的缩写;我们将在本节的后面更详细地讨论区域设置的命名方案。
区域设置类
在 Java 中,java.util.Locale
类为地区提供编程支持。表 13-1 列出了这个类中的重要方法。
表 13-1。
Important Methods in the Locale Class
方法 | 简短描述 |
---|---|
static Locale[] getAvailableLocales() |
返回 JVM 支持的可用语言环境(即已安装的语言环境)的列表。 |
static Locale getDefault() |
返回 JVM 的默认语言环境。 |
static void setDefault(Locale newLocale) |
设置 JVM 的默认语言环境。 |
String getCountry() |
返回 locale 对象的国家代码。 |
String getDisplayCountry() |
返回区域设置对象的国家名称。 |
String getLanguage() |
返回区域设置对象的语言代码。 |
String getDisplayLanguage() |
返回区域设置对象的语言名称。 |
String getVariant() |
返回区域设置对象的变量代码。 |
String getDisplayVariant() |
返回区域设置对象的变体代码的名称。 |
String toString() |
返回一个由地区语言、国家、变量等代码组成的String 。 |
清单 13-1 中的代码检测默认语言环境,并检查 JVM 中可用的语言环境。
Listing 13-1. AvailableLocales.java
import java.util.Locale;
import java.util.Arrays ;
class AvailableLocales {
public static void main(String []args) {
System.out.println("The default locale is: " + Locale.getDefault());
Locale [] locales = Locale.getAvailableLocales();
System.out.printf("No. of other available locales is: %d, and they are: %n",
locales.length);
Arrays.stream(locales).forEach(
locale -> System.out.printf("Locale code: %s and it stands for %s %n",
locale, locale.getDisplayName()));
}
}
它打印以下内容:
The default locale is: en_US
No. of other available locales is: 160, and they are:
Locale code: ms_MY and it stands for Malay (Malaysia)
Locale code: ar_QA and it stands for Arabic (Qatar)
Locale code: is_IS and it stands for Icelandic (Iceland)
Locale code: sr_RS_#Latn and it stands for Serbian (Latin,Serbia)
Locale code: no_NO_NY and it stands for Norwegian (Norway,Nynorsk)
Locale code: th_TH_TH_#u-nu-thai and it stands for Thai (Thailand,TH)
Locale code: fr_FR and it stands for French (France)
Locale code: tr and it stands for Turkish
Locale code: es_CO and it stands for Spanish (Colombia)
Locale code: en_PH and it stands for English (Philippines)
Locale code: et_EE and it stands for Estonian (Estonia)
Locale code: el_CY and it stands for Greek (Cyprus)
Locale code: hu and it stands for Hungarian
[...rest of the output elided...]
在分析输出之前,让我们看看程序中的方法。您使用Locale
中的方法getDefault()
来获取默认地区的代码。之后,在Locale
类中使用getAvailableLocales()
来获取 JVM 支持的语言环境列表。现在,为每个地区打印地区代码,并使用Locale
的getDisplayName()
方法打印描述性名称。
该程序将这个 JVM 的默认语言环境打印为en_US
,这意味着默认语言环境是美国使用的英语。然后它打印出一个很长的可用地区列表;为了节省空间,我们只展示了输出的一小部分。从这个程序中,您知道有许多可用和受支持的语言环境,并且每个 JVM 都有一个默认的语言环境。
此输出中有四种不同的区域设置代码:
- “hu 和它代表匈牙利语”:只有一个代码,其中
hu
代表匈牙利语 - “ms_MY 和它代表马来语(马来西亚)”:由下划线分隔的两个代码,其中
ms
代表马来语,MY
代表马来西亚 - “no_NO_NY,它代表挪威语(挪威,尼诺斯克)”:由下划线分隔的三个代码,如 no_NO_NY,其中
no
代表挪威语,NO
代表挪威,NY
代表尼诺斯克语 - “th_th_TH_#u-nu-thai,它代表泰语(Thailand,TH)”:由下划线分隔的两个或三个初始代码,最后一个由
#
或_#
分隔,如在th_TH_TH_#u-nu-thai
中,我们现在将讨论它。
以下是这些区域名称的编码方式:
language + "_" + country + "_" + (variant + "_#" | "#") + script + "-" + extensions
这种语言环境编码方案允许组合不同的变体来创建一个语言环境。对于“th_TH_TH_#u-nu-thai”的地区代码,
- 语言代码是
"th"
(泰语),并且总是用小写字母书写 - 国家代码是
"TH"
(泰国),并且总是用大写字母书写 - 变体名称为
"TH"
;这里它重复了国家代码,但也可以是任何字符串 - 脚本名称在这里是一个空字符串;如果给定,它将是一个四个字母的字符串,第一个字母大写,其余字母小写(例如,
Latn
) - 扩展名跟在
#
或_#
字符之后(因为脚本是一个空字符串);在这个例子中是“u-nu-thai”
再举一个例子,考虑地区代码"sr_RS_#Latn"
,
- 语言代码为
"sr"
(塞尔维亚语) - 国家代码是
"RS"
(塞尔维亚) - 此处的变量名称为空
- 脚本名为
"Latn"
(拉丁文),由四个字母组成,第一个字母大写,其余字母小写 - 这里的分机是空的
以英语为例,许多国家都讲英语。英语因使用该语言的国家而有所不同。我们都知道美式英语和英式英语不同,但是这样的版本很多。以下代码(清单 13-2 )从所有可用的语言环境中只过滤英语语言环境:
Listing 13-2. AvailableLocalesEnglish.java
import java.util.Locale;
import java.util.Arrays;
class AvailableLocalesEnglish {
public static void main(String []args) {
Arrays.stream(Locale.getAvailableLocales())
.filter(locale -> locale.getLanguage().equals("en"))
.forEach(locale ->
System.out.printf("Locale code: %s and it stands for %s %n",
locale, locale.getDisplayName()));
}
}
它会打印以下内容(输出和顺序可能会因您的机器而异):
Locale code: en_MT and it stands for English (Malta)
Locale code: en_GB and it stands for English (United Kingdom)
Locale code: en_CA and it stands for English (Canada)
Locale code: en_US and it stands for English (United States)
Locale code: en_ZA and it stands for English (South Africa)
Locale code: en and it stands for English
Locale code: en_SG and it stands for English (Singapore)
Locale code: en_IE and it stands for English (Ireland)
Locale code: en_IN and it stands for English (India)
Locale code: en_AU and it stands for English (Australia)
Locale code: en_NZ and it stands for English (New Zealand)
Locale code: en_PH and it stands for English (Philippines)
输出用英语表示不同的地区,只使用语言代码和国家代码。我们在Locale
中使用了getLanguage()
方法,它返回地区代码。还有哪些这样的方法?现在让我们探索一下Locale
类中可用的方法。
获取区域设置详细信息和设置区域设置
Locale
类中的 getter 方法如getLanguage()
、getCountry()
和getVariant()
返回代码,而类似的方法getDisplayCountry()
、getDisplayLanguage()
和getDisplayVariant()
返回名称。清单 13-3 展示了如何为地区Locale.CANADA_FRENCH
使用这些方法。
Listing 13-3. LocaleDetails.java
import java.util.Locale;
public class LocaleDetails {
public static void main(String args[]) {
Locale.setDefault(Locale.CANADA_FRENCH);
Locale defaultLocale = Locale.getDefault();
System.out.printf("The default locale is %s %n", defaultLocale);
System.out.printf("The default language code is %s and the name is %s %n",
defaultLocale.getLanguage(), defaultLocale.getDisplayLanguage());
System.out.printf("The default country code is %s and the name is %s %n",
defaultLocale.getCountry(), defaultLocale.getDisplayCountry());
System.out.printf("The default variant code is %s and the name is %s %n",
defaultLocale.getVariant(), defaultLocale.getDisplayVariant());
}
}
它打印以下内容:
The default locale is fr_CA
The default language code is fr and the name is français
The default country code is CA and the name is Canada
The default variant code is and the name is
让我们来了解一下程序。setDefault()
方法将一个Locale
对象作为参数。在这个程序中,您使用下面的语句将默认语言环境设置为Locale.CANADA_FRENCH
:
Locale.setDefault(Locale.CANADA_FRENCH);
Locale
类有许多静态的Locale
对象来表示常见的地区,这样你就不必实例化它们并直接在你的程序中使用它们。在这种情况下,Locale.CANADA_FRENCH
是一个静态的Locale
对象。
不使用这个静态的Locale
对象,您可以选择实例化一个Locale
对象。下面是通过创建新的加拿大(法语)区域设置对象来设置默认区域设置的另一种方法:
Locale.setDefault(new Locale("fr", "CA", ""));
Locale
中的getDefault()
方法返回 JVM 中的默认 locale 对象集。下一条语句使用方法获取与国家相关的信息。getCountry()
和getDisplayCountry()
方法的区别在于,前者返回国家代码(对我们来说可读性不强),后者返回国家名称,这是人类可读的。国家代码是两个或三个字母的代码(此代码来自国际标准:ISO 3166)。
getLanguage()
和getDisplayLanguage()
的行为类似于获取国家细节。语言代码由两个或三个字母组成(这个代码来自另一个国际标准:ISO 639)。
这个语言环境中没有变量,所以当您使用getVariant()
和getDisplayVariant()
方法时,不会打印任何东西。然而,对于其他一些地区,可能有不同的值,这些值将被打印出来。变量可以是任何额外的细节,如操作环境(如 MAC for Macintosh 机器)或公司名称(如 Sun 或 Oracle)。
除此之外,还有一些不太常用的方法,比如返回脚本代码和地区国家名称的getScript()
和getDisplayCountry()
。
不要调用
Locale
的getDisplayCountry()
方法,它不接受任何参数,你可以选择getDisplayCountry(Locale)
的重载版本,它接受一个Locale
对象作为参数。这将按照传递的区域设置打印国家的名称。例如,对于呼叫Locale.GERMANY.getDisplayCountry()
,您将得到输出“德国”(这是德国人对他们国家的称呼);然而,对于调用Locale.GERMANY.getDisplayCountry(Locale.ENGLISH)
,您将得到输出“德国”(这是英国人对国名德国的称呼)
Different Ways to Create a Locale Object
有许多方法可以获得或创建一个Locale
对象。我们在这里列出了四个选项,用于创建与其语言代码相对应的意大利语言环境的实例。
选项 1:使用Locale
类的构造函数:Locale(String language, String country, String variant):
Locale locale1 = new Locale("it", "", "");
选项 2:使用Locale
类中的forLanguageTag(String languageTag)
方法:
Locale locale2 = Locale.forLanguageTag("it");
选项 3:通过实例化Locale.Builder
构建一个Locale
对象,然后从该对象调用setLanguageTag()
:
Locale locale3 = new Locale.Builder().setLanguageTag("it").build();
选项 4:在Locale
类中为地区使用预定义的静态最终常量:
Locale locale4 = Locale.ITALIAN;
您可以根据需要选择创建Locale
对象的方式。例如,Locale
类只有几个针对地区的预定义常量。如果你想要一个预定义的Locale
对象,你可以直接使用它,或者你必须检查使用哪个选项。
资源包
认证目标 | |
---|---|
为每个语言环境构建一个资源包,并在应用中加载一个资源包 |
在上一节中,我们讨论了Locale
类以及获取默认语言环境细节和可用语言环境列表的方法。如何使用这些地区信息来定制程序的行为?让我们举一个简单的问候别人的例子:在英语中,你说“你好”,但如果地区不同,你如何将这个问候改为,例如,“Ciao”,如果地区是意大利语(意大利)?
一个显而易见的解决方案是获取默认的区域设置,检查区域设置是否是意大利,并打印“Ciao”这是可行的,但是这种方法既不灵活也不可扩展。如何定制到其他地区,如沙特阿拉伯(阿拉伯语)或泰国(泰语)?您必须找到并替换所有特定于语言环境的字符串,以便为每个语言环境进行定制。如果您的应用由分布在一百万行代码中的数千个这样的字符串组成,并且需要支持许多地区,那么这项任务将是一场噩梦。
在 Java 中,资源包为如何定制应用以满足特定于地区的需求提供了一个解决方案。那么,什么是资源包呢?资源包是一组帮助定义一组键并将这些键映射到特定于地区的值的类或属性文件。
抽象类java.util.ResourceBundle
提供了 Java 中资源束的抽象。它有两个派生类:java.util.PropertyResourceBundle
和java.util.ListResourceBundle
(见图 13-1 )。这两个派生类使用两种不同的机制为资源包提供支持:
图 13-1。
ResourceBundle and its two derived classes
PropertyResourceBundle
类:这个具体的类以属性文件的形式提供了对多种语言环境的支持。对于每个区域设置,您可以在该区域设置的属性文件中指定键和值。对于给定的语言环境,如果使用ResourceBundle.getBundle()
方法,相关的属性文件将被自动加载。当然,这里面没有魔法;创建属性文件必须遵循一定的命名约定,我们将在专门讨论属性文件的章节中讨论这一点。使用属性文件时,只能使用Strings
作为键和值。ListResourceBundle
类:为了增加对语言环境的支持,您可以扩展这个抽象类。在你的派生类中,你必须重写getContents()
方法,它返回一个Object [][]
。该数组必须有键和值的列表。按键必须是Strings
。通常值也是Strings
,但是值可以是任何东西:声音剪辑、视频剪辑、URL 或者图片。
让我们快速看一下ResourceBundle
抽象类支持的方法。表 13-2 总结了这个类的重要方法。我们现在将讨论使用ResourceBundle
的这两个派生类的本地化支持。
表 13-2。
Important Methods in the ResourceBundle Abstract Class
方法 | 简短描述 |
---|---|
Object getObject(String key) |
返回映射到给定键的值。如果没有找到给定键的对象,抛出一个MissingResourceException 。 |
static ResourceBundle getBundle(String baseName),``static final ResourceBundle getBundle(String baseName, Locale locale) |
返回给定的baseName 、locale 的ResourceBundle ,如果没有找到匹配的资源包,control; 抛出一个MissingResourceException 。Control 参数用于控制或获取资源包加载过程的信息 |
String getString(String key) |
返回映射到给定键的值;相当于将返回值从getObject() 强制转换为String 。如果没有找到给定键的对象,抛出一个MissingResourceException 。如果返回的对象不是一个String ,抛出ClassCastException 。 |
使用 PropertyResourceBundle
认证目标 | |
---|---|
创建并读取属性文件 |
如果您使用属性文件设计应用时考虑到了本地化,那么您可以在不更改任何代码的情况下为应用添加对新语言环境的支持!
现在我们来看一个使用资源文件的例子,你会明白的。让我们从一个非常简单的向用户输出“Hello”的程序开始。这个程序有三个属性文件资源包:
The default resource bundle that assumes the English (US) locale. A resource bundle for the Arabic locale. A resource bundle for the Italian locale.
如上所述,属性文件将字符串定义为文件中的键值对。下面是一个类路径的例子,它可以映射到机器中的一个实际路径:classpath=C:\Program Files\Java\jre8
。属性文件通常包含许多这样的键值对,每个键值对位于单独的行中,如下所示:
classpath=C:\Program Files\Java\jre8
temp=C:\Windows\Temp
windir=C:\Windows
在本地化的情况下,使用属性文件将相同的键字符串映射到不同的值字符串。在程序中,您将引用键字符串,并通过加载与该地区相匹配的属性文件,从属性文件中获取键的相应值,以便在程序中使用。
这些属性文件的命名很重要(您很快就会明白为什么),下面是这些包的内容。为了使这个例子简单,在这些属性文件中只有一个键值对;在现实世界的程序中,每个属性文件中可能有几百甚至几千对。
D:\> type ResourceBundle.properties
Greeting=Hello
D:\> type ResourceBundle_ar.properties
Greeting=As-Salamu Alaykum
D:\> type ResourceBundle_it.properties
Greeting=Ciao
如您所见,默认的包名为ResourceBundle.properties
。阿拉伯语的资源包被命名为ResourceBundle_ar.properties
。注意后缀“_ar”,表示阿拉伯语是本地语言。类似地,意大利语的资源包被命名为ResourceBundle_it.properties
,它使用"_it"
后缀来表示意大利语是这个属性文件的关联语言。清单 13-4 利用了这些资源包。
Listing 13-4. LocalizedHello.java
import java.util.Locale;
import java.util.ResourceBundle;
public class LocalizedHello {
public static void main(String args[]) {
Locale currentLocale = Locale.getDefault();
ResourceBundle resBundle =
ResourceBundle.getBundle("ResourceBundle", currentLocale);
System.out.printf(resBundle.getString("Greeting"));
}
}
图 13-2。
Getting relevant strings from ResourceBundles based on the locale
有两个选项可以按所需方式运行该程序:
- 选项 I:通过调用
setDefault()
方法来更改程序中的默认区域设置:Locale.setDefault(Locale.ITALY);
不推荐使用该选项,因为它需要更改程序来设置区域设置。 - 选项 II:从命令行调用 JVM 时更改默认的语言环境(如果您从 ide 调用 JVM,请在 IDE 设置中为 JVM 提供命令行参数):
D:\> java -Duser.language=it -Duser.region=IT LocalizedHello
让我们通过用选项 II(调用 JVM 时向命令行传递参数)设置语言环境来尝试这个程序。
D:\> java LocalizedHello
Hello
D:\> java -Duser.language=it LocalizedHello
Ciao
D:\> java -Duser.language=ar LocalizedHello
As-Salamu Alaykum
正如您所看到的,根据您显式设置的地区(本例中是意大利语或阿拉伯语),或者默认地区(本例中是美国英语),相应的属性文件被加载,消息字符串被解析。
如果您忘记创建属性文件或者它们不在路径中,您将得到一个
MissingResourceException
。
在程序中,首先在语句中获取当前的区域设置。
Locale currentLocale = Locale.getDefault();
之后,加载以名称ResourceBundle
开始的资源包,并传递加载资源包的区域设置。
ResourceBundle resBundle = ResourceBundle.getBundle("ResourceBundle", currentLocale);
最后,从资源包中,查找关键字“Greeting ”,并根据加载的资源包使用该关键字的值。
System.out.printf(resBundle.getString("Greeting"));
使用 ListResourceBundle
可以使用ListResourceBundle
通过扩展来添加对新语言环境的支持。在扩展ListResourceBundle
的时候,你需要覆盖抽象方法getContents();
,这个方法的签名是:
protected Object[][] getContents();
注意键是Strings
,但是值可以是任何类型,因此数组类型是Object
;此外,该方法返回键和值对的列表。结果,getContents()
方法返回一个二维数组Objects
。
您通过扩展
ListResourceBundle
类来创建资源包,而使用PropertyResourceBundle
,您将资源包创建为属性文件。此外,当扩展ListResourceBundle
时,可以将任何类型的对象作为值,而属性文件中的值只能是字符串。
清单 13-5 展示了一个扩展ListResourceBundle
的例子,它返回特定地区最卖座的电影。它定义了名为ResBundle
的资源包。因为类名没有任何后缀(比如"_it"
或者"_en_US"
,所以它是资源包的默认实现。在为任何地区寻找匹配的ResBundle
时,如果找不到匹配,将使用这个默认实现。
Listing 13-5. ResBundle.java
import java.util.ListResourceBundle;
// default US English version
public class ResBundle extends ListResourceBundle {
public Object[][] getContents() {
return contents;
}
static final Object[][] contents = {
{ "MovieName", "Avatar" },
{ "GrossRevenue", (Long) 2782275172L }, // in US dollars
{ "Year", (Integer)2009 }
};
}
现在,让我们为意大利地区定义一个ResBundle
。您给这个类加上后缀"_it_IT"
。语言代码"it"
代表意大利语,国家代码"IT"
代表意大利(列表 13-6 )。
Listing 13-6. ResBundle_it_IT.java
import java.util.ListResourceBundle;
// Italian version
public class ResBundle_it_IT extends ListResourceBundle {
public Object[][] getContents() {
return contents;
}
static final Object[][] contents = {
{ "MovieName", "Che Bella Giornata" },
{ "GrossRevenue", (Long) 43000000L }, // in euros
{ "Year", (Integer)2011 }
};
}
正如您所看到的,除了映射到键的值之外,ResBundle
和ResBundle_it_IT
的实现是相似的。现在,您如何知道您的资源包是否在工作呢?清单 13-7 加载默认和意大利语言环境的ResBundle
。
Listing 13-7. LocalizedBoxOfficeHits.java
import java.util.ResourceBundle;
import java.util.Locale;
public class LocalizedBoxOfficeHits {
public void printMovieDetails(ResourceBundle resBundle) {
String movieName = resBundle.getString("MovieName");
Long revenue = (Long)(resBundle.getObject("GrossRevenue"));
Integer year = (Integer) resBundle.getObject("Year");
System.out.println("Movie " + movieName + "(" + year + ")" + " grossed "
+ revenue );
}
public static void main(String args[]) {
LocalizedBoxOfficeHits localizedHits = new LocalizedBoxOfficeHits();
// print the largest box-office hit movie for default (US) locale
Locale locale = Locale.getDefault();
localizedHits.printMovieDetails(ResourceBundle.getBundle("ResBundle", locale));
// print the largest box-office hit movie for Italian locale
locale = new Locale("it", "IT", "");
localizedHits.printMovieDetails(ResourceBundle.getBundle("ResBundle", locale));
}
}
它打印以下内容:
Movie Avatar (2009) grossed 2782275172
Movie Che Bella Giornata (2011) grossed 43000000
它成功加载了默认和意大利资源包。但是,这个输出存在一些问题。值 2782275172 是美元值,值 43000000 是欧元值。而且数字打印出来没有逗号,很难搞清楚这些数字的意义。可以本地化这些货币值的格式。但是处理数字格式、十进制格式和地区间的货币差异并不包含在 OCPJP 8 考试中,所以我们在本书中不做进一步的讨论。
现在,考虑该程序中的以下语句:
Long revenue = (Long)(resBundle.getObject("GrossRevenue"));
该语句返回映射到资源包中名为GrossRevenue
的键的值。你已经在类ResBundle
和ResBundle_it_IT
中将它定义为一个Long
对象——所以它工作了。例如,如果您错误地将类型转换为Integer
,您将得到一个ClassCastException
,如下所示:
Integer revenue = (Integer)(resBundle.getObject("GrossRevenue"));
// This code change will result in throwing this exception:
// Exception in thread "main" java.lang.ClassCastException:
// java.lang.Long cannot be cast to java.lang.Integer
这里是另一种情况:如果您错误地键入了GrossRevenu
而不是GrossRevenue
作为键名,程序将会崩溃并出现这个异常,如下所示:
Long revenue = (Long)(resBundle.getObject("GrossRevenu"));
// This code will crash with this exception:
// Exception in the thread "main" java.util.MissingResourceException:
// Can't find resources for bundle ResBundle, key
GrossRevenu
为了从资源包中获取对象,在提供 keyname 时需要小心:keyname 是区分大小写的,键名应该完全匹配——否则会得到 MissingResourceException。
加载资源包
认证目标 | |
---|---|
为每个语言环境构建一个资源包,并在应用中加载一个资源包 |
您已经在使用ResourceBundle
或它的两个派生类编写的程序中加载了资源包。从考试的角度来看,您需要彻底理解这个加载过程,所以我们将在本节中更详细地讨论它。
对于从ListResourceBundles
扩展的类,寻找匹配资源包的过程与为PropertyResourceBundles
定义的属性文件相同。
对于实现为从
ListResourceBundles
扩展的类的资源包,Java 使用反射机制来查找和加载类。您需要确保该类是公共的,以便反射机制可以找到该类。
资源包的命名约定
Java 强制执行一个预定义的命名约定来创建资源包。Java 库只通过属性包的名称加载相关的语言环境。因此,在创建用于本地化 Java 应用的属性包时,理解并遵循这个命名约定是很重要的。
您已经看到了语言环境名称是如何编码的。理解这种语言环境名称编码对于命名资源包非常重要,因为它使用相同的编码方案。完全限定的资源包具有以下形式:
packagequalifier.bundlename + "_" + language + "_" + country + "_" + (variant + "_#" | "#") + script + "-" + extensions
以下是该完全限定名称中元素的描述:
packagequalifier
:提供资源包的包(或子包)的名称。bundlename
:您将在程序中引用和加载的资源包的名称。language
:两个字母的缩写,通常以小写形式表示该地区的语言(在极少数情况下,也可以是三个字母)。country
:两个字母的缩写,通常以大写形式表示该地区的国家(在极少数情况下,也可以是三个字母)。variant
:当您需要一个以上的语言和国家组合的地区时,一个任意的变量列表(小写或大写)来区分地区。
我们已经省略了对脚本和扩展的描述,因为它们很少被使用。
例如,考虑这个完全限定名:
localization.examples.AppBundle_ en_US_Oracle_exam
在本例中,localization.examples
是包,AppBundle
是资源包的名称,en
是语言(代表英语),US
是国家,Oracle_exam
是变体。
地区语言和国家的两个(有时三个)字母缩写是预定义的,因为它们是基于国际标准的。我们不提供详细的列表,也没有必要在考试中知道或记住所有的列表。您可以查看Locale
类的文档来理解这一点。
在 OCPJP 八级考试中,你不需要记住用于命名资源包的语言代码或国家代码。但是,您应该记住命名约定,并认识到完全限定的资源包名称的组成部分。
假设一个包名可能有多个资源包,那么确定要加载的资源包的搜索顺序是什么?为了清楚起见,我们将该序列表示为一系列步骤。搜索从步骤 1 开始。如果在任何步骤中搜索发现匹配,则加载资源包。否则,搜索进行到下一步(见图 13-3 )。
图 13-3。
Seach sequence for ResourceBundles
搜索从给定的区域设置细节开始,如果没有找到,则继续检查默认区域设置,如下所示:
BundleName + "_" + language + "_" + country + "_" + variant
BundleName + "_" + language + "_" + country
BundleName + "_" + language
BundleName + "_" + defaultLanguage + "_" + defaultCountry
BundleName + "_" + defaultLanguage
考虑一个例子,看看如何找到匹配的资源包,你就清楚了。假设您在搜索路径中有以下五个条目,并且您的默认区域设置是美国英语。
ResourceBundle.properties -- Global bundle
ResourceBundle_ar.properties -- Arabic language bundle
ResourceBundle_en.properties -- English bundle (assuming en_US is the default locale)
ResourceBundle_it.properties -- Italian language bundle
ResourceBundle_it_IT_Rome.properties -- Italian (Italy, Rome) bundle
getBundle()
方法将一个ResourceBundle.Control
对象作为附加参数。通过扩展这个ResourceBundle.Control
类并将该扩展类的实例传递给getBundle()
方法,您可以改变默认的资源包搜索过程或者读取非标准的资源包格式(比如 XML 文件)。
我们如何才能找出 Java 正在搜索的语言环境的顺序呢?为此,让我们扩展ResourceBundle.Control
类并覆盖getCandidateLocales()
方法:这是为了以编程方式访问和打印候选地区列表,并最终显示匹配的地区。请注意,“候选语言环境”是指 Java 在搜索过程中考虑的语言环境。假设名为ResourceBundle_it_IT_Rome.properties
和ResourceBundle_en.properties
的属性文件可用。该程序在清单 13-8 中给出。
Listing 13-8. CandidateLocales.java
import java.util.ResourceBundle;
import java.util.List;
import java.util.Locale;
// Extend ResourceBundle.Control and override getCandidateLocales method
// to get the list of candidate locales that Java searches for
class TalkativeResourceBundleControl extends ResourceBundle.Control {
// override the default getCandidateLocales method to print
// the candidate locales first
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
List<Locale> candidateLocales = super.getCandidateLocales(baseName, locale);
System.out.printf("Candidate locales for base bundle name %s and locale %s %n",
baseName, locale.getDisplayName());
candidateLocales.forEach(System.out::println);
return candidateLocales;
}
}
// Use a helper method loadResourceBundle to load a bundle given the bundle name and locale
class CandidateLocales {
public static void loadResourceBundle(String resourceBundleName, Locale locale) {
// Pass an instance of TalkativeResourceBundleControl
// to print candidate locales
ResourceBundle resourceBundle = ResourceBundle.getBundle(resourceBundleName,
locale, new TalkativeResourceBundleControl());
String rbLocaleName = resourceBundle.getLocale().toString();
// if the resource bundle locale name is empty,
// it means default property file
if(rbLocaleName.equals("")) {
System.out.println("Loaded the default property file with name: "
+ resourceBundleName);
} else {
System.out.println("Loaded the resource bundle for the locale: "
+ resourceBundleName + "." + rbLocaleName);
}
}
public static void main(String[] args) {
// trace how ResourceBundle_it_IT_Rome.properties is resolved
loadResourceBundle("ResourceBundle", new Locale("it", "IT", "Rome"));
}
}
它打印以下内容:
Candidate locales for base bundle name ResourceBundle and locale Italian (Italy, Rome)
it_IT_Rome
it_IT
it
Loaded the resource bundle for the locale: ResourceBundle.it_IT_Rome
现在,在尝试其他语言环境之前,考虑一下程序是如何工作的。为了跟踪 Java 如何解析最终加载的资源包,您需要获得候选语言环境的列表。使用ResourceBundle.getBundle()
方法,您可以传递一个额外的参数,它是ResourceBundle.Control
类的一个实例。出于这个原因,您定义了TalkativeResourceBundleControl
类。
TalkativeResourceBundleControl
类扩展了ResourceBundle.Control
类并覆盖了getCandidateLocales()
方法。这个getCandidateLocales()
方法返回一个List<Locale>
实例,其中包含给定地区的候选地区列表。您调用super.getCandidateLocales()
并遍历产生的List<Locale>
对象来打印候选语言环境,以便稍后可以检查输出。从这个被覆盖的getCandidateLocales()
方法中,您只需返回这个List<Locale>
对象。因此,TalkativeResourceBundleControl
的行为与ResourceBundle.Control
相同,除了TalkativeResourceBundleControl
中被覆盖的getCandidateLocales()
打印候选语言环境。
CandidateLocales
类利用了TalkativeResourceBundleControl
。它有一个名为loadResourceBundle()
的助手方法,该方法将资源包名称和地区名称作为参数。这个方法只是将这些参数值传递给ResourceBundle.getBundle()
方法;此外,它实例化了TalkativeResourceBundleControl
,并将该对象作为第三个参数传递给该方法。getBundle()
方法返回一个ResourceBundle
对象。如果ResourceBundle.getLocale()
名称的区域设置为空,则意味着 Java 已经加载了全局资源包。(请记住,该包名称的全局资源包没有任何相关的区域设置细节。)如果语言环境的名称不为空,则意味着 Java 已经解析到该特定的语言环境。
现在,考虑一下main()
方法中的代码。它为地区it_IT_Rome
调用loadResourceBundle()
。有三个候选语言环境,其中它正确地加载了与语言环境it_IT_Rome
匹配的属性文件。所以你知道它正确地加载了属性文件ResourceBundle_it_IT_Rome.properties
。
为了继续这个实验,让我们将清单 13-8 的main()
方法中的代码改为如下代码:
loadResourceBundle("ResourceBundle", new Locale("fr", "CA", ""));
现在,该程序打印以下内容:
Candidate locales for base bundle name ResourceBundle and locale French (Canada)
fr_CA
fr
Candidate locales for base bundle name ResourceBundle and locale English (United States)
en_US
en
Loaded the resource bundle for the locale: ResourceBundle.en
为什么程序会打印出上面的输出?注意,在属性文件列表中没有对应于fr_CA
地区的属性文件。因此,搜索将继续检查默认语言环境的属性文件。在这种情况下,默认的地区是en_US
,并且有一个属性文件用于en
(英语)地区。因此,从候选语言环境中,Java 决定正确地加载属性文件ResourceBundle_en.properties
。
这是最后一个例子。用以下语句替换main()
方法中的代码:
loadResourceBundle("ResBundl", Locale.getDefault());
该程序打印以下内容:
Candidate locales for base bundle name ResBundl and locale English (United States)
en_US
en
The exception in thread "main" java.util.MissingResourceException: Can't find bundle for base name ResBundl, locale en_US
[... thrown stack trace elided ...]
您没有任何名为ResBundl
的资源包,并且您已经给出了默认的语言环境(在本例中为en_US
)。Java 搜索这个地区的包,您知道您没有提供任何名为ResBundl
的包。所以,程序在抛出一个MissingResourceException
后崩溃。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
使用 locale 对象读取和设置区域设置
- 区域设置代表一种语言、文化或国家;Java 中的
Locale
类为这个概念提供了一个抽象。 - 每个区域设置可以有三个条目:语言、国家和变体。您可以使用适用于语言和国家的标准代码来构成区域设置标签。变体没有标准标签;您可以根据需要提供不同的字符串。
Locale
类中的 getter 方法——如getLanguage()
、getCountry(),
、getVariant()
——返回代码;而getDisplayCountry()
、getDisplayLanguage()
、getDisplayVariant()
的类似方法返回名字。Locale
中的getDefault()
方法返回 JVM 中的默认区域设置。您可以使用setDefault()
方法将这个默认语言环境更改为另一个语言环境。- 有许多方法可以创建或获取对应于一个地区的
Locale
对象:- 使用
Locale
类的构造函数。 - 使用
Locale
类中的forLanguageTag(String languageTag)
方法。 - 通过实例化
Locale.Builder
构建一个Locale
对象,然后从该对象调用setLanguageTag()
。 - 在
Locale
类中为区域设置使用预定义的静态 final 常量。
- 使用
创建并读取属性文件
- 资源包是一组帮助定义一组键并将这些键映射到特定于地区的值的类或属性文件。
- 类
ResourceBundle
有两个派生类:PropertyResourceBundle
和ListResourceBundle
。您可以使用ResourceBundle.getBundle()
来获取给定地区的包。 The PropertyResourceBundle
类以属性文件的形式提供对多种语言环境的支持。对于每个区域设置,您可以在该区域设置的属性文件中指定键和值。您只能使用String
作为键和值。- 要添加对新语言环境的支持,您可以扩展
ListResourceBundle
类。在这个派生类中,你必须重写the Object [][] getContents()
方法。返回的数组必须包含键和值的列表。键必须是字符串,值可以是任何对象。 - 当将键字符串传递给
getObject()
方法以获取资源包中的匹配值时,确保传递的键和资源包中的键完全匹配(keyname 区分大小写)。如果不匹配,你会得到一个MissingResourceException
。 - 完全限定的资源包名称的命名约定是
packagequalifier.bundlename + "_" + language + "_" + country + "_" + (variant + "_#" | "#") + script + "-" + extensions
。
为每个语言环境构建一个资源包,并在应用中加载一个资源包
- 对于从
ListResourceBundles
扩展的类,寻找匹配资源包的过程与为PropertyResourceBundles
定义的属性文件相同。 - 下面是查找匹配资源包的搜索序列。搜索从步骤 1 开始。如果在任何步骤中搜索发现匹配,则加载资源包。否则,搜索进行到下一步。
- 步骤 1:搜索从寻找与资源束完全匹配的全名开始。
- 步骤 2:删除最后一个组件(由 _ 分隔的部分),并使用得到的较短名称重复搜索。重复这个过程,直到剩下最后一个区域设置修饰符。
- 步骤 3:使用默认地区的包的全名继续搜索。
- 步骤 4:只使用包的名称搜索资源包。
- 第五步:搜索失败,抛出一个
MissingBundleException
。
getBundle()
方法将一个ResourceBundle.Control
对象作为附加参数。通过扩展这个ResourceBundle.Control
类并传递那个对象,您可以控制或定制资源包搜索和加载过程。
Question TimeWhich one of the following statements makes use of a factory method? Locale locale1 = new Locale("it", "", "");
NumberFormat.getInstance(Locale.GERMANY);
Locale locale3 = new Locale.Builder().setLanguageTag("it").build();
Date today = new Date();
Locale locale4 = Locale.ITALIAN;
Consider the following program and choose the correct option: import java.util.Locale;
class Test {
public static void main(String []args) {
Locale locale1 = new Locale("en"); //#1
Locale locale2 = new Locale("en", "in"); //#2
Locale locale3 = new Locale("th", "TH", "TH"); //#3
Locale locale4 = new Locale(locale3); //#4
System.out.println(locale1 + " " + locale2 + " " + locale3 + " " + locale4);
}
}
This program will print the following: en en_IN th_TH_TH_#u-nu-thai th_TH_TH_#u-nu-thai. This program will print the following: en en_IN th_TH_TH_#u-nu-thai (followed by a runtime exception). This program results in a compiler error at statement #1. This program results in a compiler error at statement #2. This program results in a compiler error at statement #3. This program results in a compiler error at statement #4. Choose the best option based on this program: import java.util.
Locale;
class LocaleTest {
public static void main(String []args) {
Locale locale = new Locale("navi", "pandora"); //#1
System.out.println(locale);
}
}
The program results in a compiler error at statement #1. The program results in a runtime exception of NoSuchLocaleException
. The program results in a runtime exception of MissingResourceException
. The program results in a runtime exception of IllegalArgumentException
. The program prints the following: navi_PANDORA. For localization, resource bundle property files are created that consist of key-value pairs. Which one of the following is a valid key value pair as provided in a resource bundle property file for some strings mapped to German language? A.
<pair> <key>from</key> <value>von</value> </pair>
<pair> <key>subject</key> <value> betreff </value> </pair>
B.
from=von
subject=betreff
C.
key=from; value=von
key=subject; value=betreff
D.
pair<from,von>
pair<subject,betreff>
Assume that you’ve the following resource bundles in your classpath: ResourceBundle.properties
ResourceBundle_ar.properties
ResourceBundle_en.properties
ResourceBundle_it.properties
ResourceBundle_it_IT_Rome.properties
Also assume that the default locale is English (US), where the language code is en and country code is US. Which one of these five bundles will be loaded for the call loadResourceBundle("ResourceBundle", new Locale("fr", "CA", ""));?
ResourceBundle.properties
ResourceBundle_ar.properties
ResourceBundle_en.properties
ResourceBundle_it.properties
ResourceBundle_it_IT_Rome.properties
Which of the following is a correct override for extending the ListResourceBundle
class? A. public HashMap<String, String> getContents() {
Map<String, String> contents = new HashMap<>();
contents.add("MovieName", "Avatar");
return contents;
}
B. public Object[] getContents() {
return new Object[] { { "MovieName" } , { "Avatar" } };
}
C. public Object[][] getContents() {
return new Object[][] { { "MovieName", "Avatar" } };
}
D. public String[] getKeysAndValues() {
return new String[] { { "MovieName" } , { "Avatar" } };
}
E. public String[] getProperties() {
return new String[] { { "MovieName" }, { "Avatar" } };
}
答案:
B. NumberFormat.getInstance(Locale.GERMANY);
A factory method creates an instance and returns back. Using a constructor directly to create an object is not related to a factory method, so A) and D) are not correct. C) builds a locale and is an example for using the Builder pattern. E) merely accesses the predefined Locale
object; so it’s not a method. F. This program results in a compiler error at #4. The Locale
class supports three constructors that are used in statements #1, #2, and #3; however, there is no constructor in the Locale
class that takes another Locale
object as argument, so the compiler gives an error for statement #4. E. The program prints the following: navi_PANDORA. To create a Locale
object using the constructor Locale(String language, String country)
, any String
values can be passed. Just attempting to create a Locale object will not result in throwing exceptions other than a NullPointerException
, which could be raised for passing null String
s. The toString()
method of Locale
class returns a string representation of the Locale
object consisting of language, country, variant, etc.) B. from=von
subject=betreff
In the resource bundle property files, the key values are separated using the = symbol, with each line in the resource file separated by a newline character. C. ResourceBundle_en.properties Java looks for candidate locales for a base bundle namedResourceBundle
and locale French (Canada), and checks for the presence of the following property files: ResourceBundle_fr_CA.properties
ResourceBundle_fr.properties
Since both of them are not there, Java searches for candidate locales for the base bundle named ResourceBundle
and a default locale (English - United States): ResourceBundle_en_US.properties
ResourceBundle_en.properties
Java
finds that there is a matching resource bundle, ResourceBundle_en.properties. Hence it loads this resource bundle.
C. public Object[][] getContents() {
return new Object[][] { { "MovieName", "Avatar" } };
}
The return type of the getContents()
method is Object[][]
. Further, the method should return a new object of type Object [][] from the method body
. Hence, option C) is the correct answer.
十四、模拟考试
参加 OCPJP 八级考试的前景在你心中引发了许多问题。
- "考试中会问哪些类型的问题?"
- "问题的形式是什么?"
- “题目有多难?”
- “我如何知道我是否准备好参加考试?”
本章提供了一个模拟考试来帮助回答这些问题。用这个模拟考试作为一个智力测试,来衡量你为通过 OCPJP 八级考试做了多少准备。
这次模拟考试中的问题非常接近你在 OCPJP 8 考试中会遇到的实际问题。例如,你会在实际的 OCPJP 八级考试中发现这些方面:问题将假设必要的重要陈述被包括在内;大多数问题将只包含相关的代码段(而不是完整的程序);而且问题会随机出现(而且不是按照考试大纲给的考试题目顺序)。在这次模拟考试中,我们采用了类似的方法,使这次模拟考试非常接近真实的 OCPJP 八级考试中的问题格式。
在开始之前,请从考试结束时给出的答题纸上打印一份答案。通过模拟真实的考试条件,就像参加真正的 OCPJP 八级考试一样参加这次考试。找一个安静的地方,在那里你可以不受干扰或分心地参加模拟考试。标记你的开始和结束时间,如果超过考试时间限制(2.5 小时)就停止。遵守闭卷规则:在这次模拟考试中,不要查阅答案或任何其他印刷、人力或网络资源。完成考试后再检查答案。在 85 个问题中,您需要正确回答至少 55 个问题才能通过本次考试(及格分数为 65%)。
祝你好运!
Time: 2 hours 30 minutesNo. of questions: 85What will be the result of executing this code segment ? Stream.of("ace ", "jack ", "queen ", "king ", "joker ")
.mapToInt(card -> card.length())
.filter(len -> len > 3)
.peek(System.out::print)
.limit(2);
This code segment prints: jack queen king joker
This code segment prints: jack queen
This code segment prints: king joker
This code segment does not print anything on the console Consider the following snippet: int ch = 0;
try (FileReader inputFile = new FileReader(file)) {
// #1
System.out.print( (char)ch );
}
}
Which one of the following statements can be replaced with statement #1 so that the contents of the file are correctly printed on the console and the program terminates. while( (ch = inputFile.read()) != null) {
while( (ch = inputFile.read()) != -1) {
while( (ch = inputFile.read()) != 0) {
while( (ch = inputFile.read()) != EOF) {
What will be the output of the following program? class Base {
public Base() {
System.out.println("Base");
}
}
class Derived extends Base {
public Derived() {
System.out.println("Derived");
}
}
class DeriDerived extends Derived {
public DeriDerived() {
System.out.println("DeriDerived");
}
}
class Test {
public static void main(String []args) {
Derived b = new DeriDerived();
}
}
Base Derived DeriDerived Derived DeriDerived DeriDerived Derived Base DeriDerived Derived DeriDerived Given this code segment: final
CyclicBarrier barrier
=
new CyclicBarrier(3, () -> System.out.println("Let's play")); // LINE_ONE
Runnable r = () -> { // LINE_TWO
System.out.println("Awaiting");
try {
barrier.await();
} catch(Exception e) { /* ignore */ }
};
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
Choose the correct option based on this code segment. This code segment results in a compiler error in line marked with the comment LINE_ONE
This code segment results in a compiler error in line marked with the comment LINE_TWO
This code prints: Let's play
This code prints: Awaiting
Awaiting
Awaiting
Let's play
This code segment does not print anything on the console Given this class definition : class Point {
private int x = 0, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// DEFAULT_CTOR
}
Which one of the following definitions of the Point
constructor can be replaced without compiler errors in place of the comment DEFAULT_CTOR
? a) public Point() {
this(0, 0);
super();
}
b) public Point() {
super();
this(0, 0);
}
c) private Point() {
this(0, 0);
}
d) public Point() {
this();
}
e) public Point() {
this(x, 0);
}
Consider the following program: class Base {
public Base() {
System.out.print("Base ");
}
public Base(String s) {
System.out.print("Base: " + s);
}
}
class Derived extends Base {
public Derived(String s) {
super(); // Stmt-1
super(s); // Stmt-2
System.out.print("Derived ");
}
}
class Test {
public static void main(String []args) {
Base a = new Derived("Hello ");
}
}
Select three correct options from the following list: Removing only Stmt-1
will make the program compilable and it will print the following: Base Derived
Removing only Stmt-1
will make the program compilable and it will print the following: Base: Hello Derived
Removing only Stmt-2
will make the program compilable and it will print the following: Base Derived
Removing both Stmt-1
and Stmt-2
will make the program compilable and it will print the following: Base Derived
Removing both Stmt-1
and Stmt-2
will make the program compilable and it will print the following: Base: Hello Derived
Consider the following program and choose the right option from the given list: class Base {
public void test() {
protected int a = 10; // #1
}
}
class Test extends Base { // #2
public static void main(String[] args) {
System.out.printf(null); // #3
}
}
The compiler will report an error at statement marked with the comment #1 The compiler will report an error at statement marked with the comment #2 The compiler will report errors at statement marked with the comment #3 The program will compile without any error Given this code segment: LocalDate joiningDate = LocalDate.of(2014, Month.SEPTEMBER, 20);
LocalDate now = LocalDate.of(2015, Month.OCTOBER, 20);
// GET_YEARS
System.out.println(years);
Which one of the following statements when replaced by the comment GET_ YEARS will print 1 on the console? Period years = Period.between(joiningDate, now).getYears();
Duration years = Period.between(joiningDate, now).getYears();
int years = Period.between(joiningDate, now).getYears();
Instant years = Period.between(joiningDate, now).getYears();
Consider the following program: class Outer {
class Inner {
public void print() {
System.out.println("Inner: print");
}
}
}
class Test {
public static void main(String []args) {
// Stmt#1
inner.print();
}
}
Which one of the following statements will you replace with // Stmt#1
to make the program compile and run successfully to print “Inner: print” in console? Outer.Inner inner = new Outer.Inner();
Inner inner = new Outer.Inner();
Outer.Inner inner = new Outer().Inner();
Outer.Inner inner = new Outer().new Inner();
Consider the following program: public class Outer {
private int mem = 10;
class Inner {
private int imem = new Outer().mem; // ACCESS1
}
public static void main(String []s) {
System.out.println(new Outer().new Inner().imem); // ACCESS2
}
}
Which one of the following options is correct? When compiled, this program will result in a compiler error in line marked with comment ACCESS1
When compiled, this program will result in a compiler error in line marked with comment ACCESS2
When executed, this program prints 10 When executed, this program prints 0 Consider the following program: interface EnumBase
{ }
enum AnEnum implements EnumBase { // IMPLEMENTS_INTERFACE
ONLY_MEM;
}
class EnumCheck {
public static void main(String []args) {
if(AnEnum.ONLY_MEM instanceof AnEnum) {
System.out.println("yes, instance of AnEnum");
}
if(AnEnum.ONLY_MEM instanceof EnumBase) {
System.out.println("yes, instance of EnumBase");
}
if(AnEnum.ONLY_MEM instanceof Enum) { // THIRD_CHECK
System.out.println("yes, instance of Enum");
}
}
}
Which one of the following options is correct? This program results in a compiler error in the line marked with comment IMPLEMENTS_INTERFACE
This program results in a compiler in the line marked with comment THIRD_CHECK
When executed, this program prints the following: yes, instance of AnEnum
When executed, this program prints the following: yes, instance of AnEnum
yes, instance of EnumBase
When executed, this program prints the following: yes, instance of AnEnum
yes, instance of EnumBase
yes, instance of Enum
Which of the following statements are true with respect to enums? (Select all that apply.) An enum
can have private
constructor An enum
can have public
constructor An enum
can have public
methods and fields An enum
can implement an interface An enum
can extend a class Choose the correct option based on this program: class base1 {
protected int var;
}
interface base2 {
int var = 0; // #1
}
class Test extends base1 implements base2 { // #2
public static void main(String args[]) {
System.out.println("var:" + var); // #3
}
}
The program will report a compilation error at statement marked with the comment #1 The program will report a compilation error at statement marked with the comment #2 The program will report a compilation error at statement marked with the comment #3 The program will compile without any errors Consider the following program: class WildCard {
interface BI {}
interface DI extends BI {}
interface DDI extends DI {}
static class C<T> {}
static void foo(C<? super DI> arg) {}
public static void main(String []args) {
foo(new C<BI>()); // ONE
foo(new C<DI>()); // TWO
foo(new C<DDI>()); // THREE
foo(new C()); // FOUR
}
}
Which of the following options are correct? Line marked with comment ONE
will result in a compiler error Line marked with comment TWO
will result in a compiler error Line marked with comment THREE
will result in a compiler error Line marked with comment FOUR
will result in a compiler error Consider the following definitions: interface BI {}
interface DI extends BI {}
The following options provide definitions of a template class X. Which one of the options specifies class X with a type parameter whose upper bound declares DI to be the super type from which all type arguments must be derived? class X <T super DI> { }
class X <T implements DI> { }
class X <T extends DI> { }
class X <T extends ? & DI> { }
In the context of Singleton pattern, which one of the following statements is true? A Singleton
class must not have any static members A Singleton
class has a public constructor A Factory
class may use Singleton
pattern All methods of the Singleton
class must be private Consider the following program: class ClassA {}
interface InterfaceB {}
class ClassC {}
class Test extends ClassA implements InterfaceB {
String msg;
ClassC classC;
}
Which one of the following statements is true? Class Test
is related with ClassA
with a HAS-A relationship. Class Test
is related to ClassC
with a composition relationship. Class Test
is related with String
with an IS-A relationship. Class ClassA
is related with InterfaceB
with an IS-A relationship. Choose the correct option based on the following code segment: Comparator<String> comparer =
(country1, country2) ->
country2.compareTo(country2); // COMPARE_TO
String[] brics = {"Brazil", "Russia", "India", "China"};
Arrays.sort(brics, null);
Arrays.stream(brics).forEach(country -> System.out.print(country + " "));
The program results in a compiler error in the line marked with the comment COMPARE_TO
The program prints the following: Brazil Russia India China The program prints the following: Brazil China India Russia The program prints the following: Russia India China Brazil The program throws the exception InvalidComparatorException
The program throws the exception InvalidCompareException
The program throws the exception NullPointerException
Which one of the following class definitions will compile without any errors? a) class P<T> {
static T s_mem;
}
b) class Q<T> {
T mem;
public Q(T arg) {
mem = arg;
}
}
c) class R<T> {
T mem;
public R() {
mem = new T();
}
}
d) class S<T> {
T []arr;
public S() {
arr = new T[10];
}
}
In a class that extends ListResourceBundle
, which one of the following method definitions correctly overrides the getContents()
method of the base class? a) public String[][] getContents() {
return new Object[][] { { "1", "Uno" }, { "2", "Duo" }, { "3", "Trie" }};
}
b) public Object[][] getContents() {
return new Object[][] { { "1", "Uno" }, { "2", "Duo" }, { "3", "Trie" }};
}
c) private List<String> getContents() {
return new ArrayList (Arrays.AsList({ { "1", "Uno" }, { "2", "Duo" }, { "3", "Trie" }});
}
d) protected Object[] getContents(){
return new String[] { "Uno", "Duo", "Trie" };
}
Which one of the following interfaces declares a single abstract method named iterator()?
(Note: Implementing this interface allows an object to be the target of the for-each statement.) Iterable<T>
Iterator<T>
Enumeration<E>
ForEach<T>
Choose the correct option based on this program: import java.util.stream.Stream;
public class Reduce {
public static void main(String []args) {
Stream<String> words = Stream.of("one", "two", "three");
int len = words.mapToInt(String::length).reduce(0, (len1, len2) -> len1 + len2);
System.out.println(len);
}
}
This program does not compile and results in compiler error(s) This program prints: onetwothree This program prints: 11 This program throws an IllegalArgumentException
Which one of the following options is best suited for generating random numbers in a multi-threaded application? Using java.lang.Math.random()
Using java.util.concurrent.ThreadLocalRandom
Using java.util.RandomAccess
Using java.lang.ThreadLocal<T>
Given this code segment: DateTimeFormatter fromDateFormat = DateTimeFormatter.ofPattern("MM/dd/yyyy");
// PARSE_DATE
DateTimeFormatter toDateFormat = DateTimeFormatter.ofPattern("dd/MMM/YY");
System.out.println(firstOct2015.format(toDateFormat));
Which one of the following statements when replaced with the comment PARSE_ DATE will result in the code to print “ 10/Jan/15
”? DateTimeFormatter firstOct2015 = DateTimeFormatter.parse("01/10/2015", fromDateFormat);
LocalTime firstOct2015 = LocalTime.parse("01/10/2015", fromDateFormat);
Period firstOct2015 = Period.parse("01/10/2015", fromDateFormat);
LocalDate firstOct2015 = LocalDate.parse("01/10/2015", fromDateFormat);
Consider the following program: import java.util.*;
class ListFromVarargs {
public static <T> List<T> asList1(T... elements) {
ArrayList<T> temp = new ArrayList<>();
for(T element : elements) {
temp.add(element);
}
return temp;
}
public static <T> List<?> asList2(T... elements) {
ArrayList<?> temp = new ArrayList<>();
for(T element : elements) {
temp.add(element);
}
return temp;
}
public static <T> List<?> asList3(T... elements) {
ArrayList<T> temp = new ArrayList<>();
for(T element : elements) {
temp.add(element);
}
return temp;
}
public static <T> List<?> asList4(T... elements) {
List<T> temp = new ArrayList<T>();
for(T element : elements) {
temp.add(element);
}
return temp;
}
}
Which of the asList
definitions in this program will result in a compiler error? The definition of asList1
will result in a compiler error The definition of asList2
will result in a compiler error The definition of asList3
will result in a compiler error The definition of asList4
will result in a compiler error None of the definitions (asList1, asList2, asList3, asList4
) will result in a compiler error Given this code segment: IntFunction<UnaryOperator<Integer>> func = i -> j -> i * j;
// LINE
System.out.println(apply);
Which one of these statements when replaced by the comment marked with LINE will print 200? Integer apply = func.apply(10).apply(20);
Integer apply = func.apply(10, 20);
Integer apply = func(10 , 20);
Integer apply = func(10, 20).apply();
Given this code segment: List<Map<List<Integer>, List<String>>> list = new ArrayList<>(); // ADD_MAP
Map<List<Integer>, List<String>> map = new HashMap<>();
list.add(null); // ADD_NULL
list.add(map);
list.add(new HashMap<List<Integer>, List<String>>()); // ADD_HASHMAP
list.forEach(e -> System.
out
.print(e + " ")); // ITERATE
Which one of the following options is correct? This program will result in a compiler error in line marked with comment ADD_MAP
This program will result in a compiler error in line marked with comment ADD_HASHMAP
This program will result in a compiler error in line marked with comment ITERATE
When run, this program will crash, throwing a NullPointerException
in line marked with comment ADD_NULL
When run, this program will print the following: null {} {}
Given this code snippet: LocalDate dateOfBirth = LocalDate.of(1988, Month.NOVEMBER, 4);
MonthDay monthDay =
MonthDay.of(dateOfBirth.getMonth(), dateOfBirth.getDayOfMonth());
boolean ifTodayBirthday =
monthDay.equals(MonthDay.from(LocalDate.now())); // COMPARE
System.out.println(ifTodayBirthday ? "Happy birthday!" : "Yet another day!");
Assume that today’s date is 4 th November 2015. Choose the correct answer based on this code segment. This code will result in a compiler error in the line marked with the comment COMPARE
When executed, this code will throw DateTimeException
This code will print: Happy birthday!
This code will print: Yet another day!
Consider the following program: class Base<T> { }
class Derived<T> { }
class Test {
public static void main(String []args) {
// Stmt #1
}
}
Which statements can be replaced with // Stmt#1
and the program remains compilable (choose two): Base<Number> b = new Base<Number>();
Base<Number> b = new Derived<Number>();
Base<Number> b = new Derived<Integer>();
Derived<Number> b = new Derived<Integer>();
Base<Integer> b = new Derived<Integer>();
Derived<Integer> b = new Derived<Integer>();
Which of the following classes in the java.util.concurrent.atomic
package inherit from java.lang.Number
? (Select all that apply.) AtomicBoolean
AtomicInteger
AtomicLong
AtomicFloat
AtomicDouble
Given the class definition: class Student{
public Student(int r) {
rollNo = r;
}
int rollNo;
}
Choose the correct option based on this code segment: HashSet<Student> students = new HashSet<>();
students.add(new Student(5));
students.add(new Student(10));
System.out.println(students.contains(new Student(10)));
This program prints the following: true
This program prints the following: false
This program results in compiler error(s) This program throws NoSuchElementException
Which of the following statements are true regarding resource bundles in the context of localization? (Select ALL that apply.) java.util.ResourceBundle
is the base class and is an abstraction of resource bundles that contain locale-specific objects java.util.PropertyResourceBundle
is a concrete subclass of java.util.ResourceBundle
that manages resources for a locale using strings provided in the form of a property file Classes extending java.util.PropertyResourceBundle
must override the getContents()
method which has the return type Object [][]
java.util.ListResourceBundle
defines the getKeys()
method that returns enumeration of keys contained in the resource bundle Which of the following statements is true regarding the classes or interfaces defined in the java.util.concurrent
package? (Select ALL that apply.) The Executor
interface declares a single method execute(Runnable command)
that executes the given command at sometime in the future The Callable
interface declares a single method call()
that computes a result The CopyOnWriteArrayList
class is not thread-safe unlike ArrayList
that is thread-safe The CyclicBarrier
class allows threads to wait for each other to reach a common barrier point Given these two class declarations : class CloseableImpl implements Closeable {
public void close() throws IOException {
System.out.println("In CloseableImpl.close()");
}
}
class AutoCloseableImpl implements AutoCloseable {
public void close() throws Exception {
System.out.println("In AutoCloseableImpl.close()");
}
}
Choose the correct option based on this code segment : try (Closeable closeableImpl = new CloseableImpl();
AutoCloseable autoCloseableImpl = new AutoCloseableImpl()) {
} catch (Exception ignore) {
// do nothing
}
finally {
// do nothing
}
This code segment does not print any output in console This code segment prints the following output: In AutoCloseableImpl.close()
This code segment prints the following output: In AutoCloseableImpl.close()
In CloseableImpl.close()
This code segment prints the following output: In CloseableImpl.close()
In AutoCloseableImpl.close()
Choose the correct option based on this code segment : List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
ints.replaceAll(i -> i * i); // LINE
System.out.println(ints);
This code segment prints: [1, 2, 3, 4, 5]
This program prints: [1, 4, 9, 16, 25]
This code segment throws java.lang.UnsupportedOperationException
This code segment results in a compiler error in the line marked with the comment LINE
Choose the correct option for this code snippet : public static void main(String []files) {
try (FileReader inputFile = new FileReader(new File(files[0]))) { // #1
inputFile.close(); // #2
}
catch (FileNotFoundException | IOException e) { // #3
e.printStackTrace();
}
}
The code snippet will compile without any errors The compiler will report an error at statement marked with the comment #1 The compiler will report an error at statement marked with the comment #2 The compiler will report an error at statement marked with the comment #3 Given this program: import java.time.*;
import java.time.temporal.ChronoUnit;
class DecadeCheck {
public static void main(String []args) {
Duration tenYears = ChronoUnit.YEARS.getDuration().multipliedBy(10);
Duration aDecade = ChronoUnit.DECADES.getDuration();
assert tenYears.equals(aDecade) : "10 years is not a decade!";
}
}
Assume that this program is invoked as follows: java DecadeCheck
Choose the correct option based on this program: This program does not compile and results in compiler error(s) When executed, this program prints: 10 years is not a decade!
When executed, this program throws an AssertionError
with the message “10 years is not a decade!
” When executed, this program does not print any output and terminates normally Consider the following code segment: while( (ch = inputFile.read()) != VALUE) {
outputFile.write( (char)ch );
}
Assume that inputFile
is of type FileReader
, and outputFile
is of type FileWriter
, and ch
is of type int
. The method read()
returns the character if successful, or VALUE
if the end of the stream has been reached. What is the correct value of this VALUE
checked in the while
loop for end-of-stream? -1
0
255
Integer.MAX_VALUE
Integer.MIN_VALUE
Consider the following code snippet. String srcFile = "Hello.txt";
String dstFile = "World.txt";
try (BufferedReader inputFile = new BufferedReader(new FileReader(srcFile));
BufferedWriter outputFile = new BufferedWriter(new FileWriter(dstFile))) {
int ch = 0;
inputFile.skip(6);
while( (ch = inputFile.read()) != -1) {
outputFile.write( (char)ch );
}
outputFile.flush();
} catch (IOException exception) {
System.err.println("Error " + exception.getMessage());
}
Assume that you have a file named Hello.txt in the current directory with the following contents: Hello World!
Which one of the following options correctly describes the behavior of this code segment (assuming that both srcFile
and dstFile
are opened successfully)? The program will throw an IOException
because skip()
is called before calling read()
The program will result in creating the file World.txt
with the contents “World!” in it This program will result in throwing CannotSkipException
This program will result in throwing IllegalArgumentException
Consider the following code segment: try (BufferedReader inputFile = new BufferedReader(new FileReader(srcFile));
BufferedWriter outputFile
= new BufferedWriter(new FileWriter(dstFile))) { // TRY-BLOCK
int ch = 0;
while( (ch = inputFile.read()) != -1) { // COND-CHECK
outputFile.write( (char)ch );
}
} catch (Exception exception) {
System.err.println("Error in opening or processing file "
+ exception.getMessage());
}
Assume that srcFile
and dstFile
are Strings. Choose the correct option. This program will get into an infinite loop because the condition check for end-of-stream (checking != -1)
is incorrect This program will get into an infinite loop because the variable ch
is declared as int
instead of char
This program will result in a compiler error in line marked with comment TRY-BLOCK because you need to use , (comma) instead of ; (semi-colon) as separator for opening multiple resources This program works fine and copies srcFile
to dstFile
Given the following definitions: interface InterfaceOne<T> {
void foo();
}
interface InterfaceTwo<T> {
T foo();
}
interface InterfaceThree<T> {
void foo(T arg);
}
interface InterfaceFour<T> {
T foo(T arg);
}
public class DateLambda {
public static void main(String []args) {
// STATEMENT
System.out.println(val.foo());
}
}
Which one of the following statements can be replaced with the line marked with the comment STATEMENT that the program will print the result that is same as the call LocalDateTime.now()
? InterfaceOne<LocalDateTime> val = LocalDateTime::now;
InterfaceTwo<LocalDateTime> val = LocalDateTime::now;
InterfaceThree<LocalDateTime> val = LocalDateTime::now;
InterfaceFour<LocalDateTime> val = LocalDateTime::now;
Which one of the following statements will compile without errors? Locale locale1 = new Locale.US;
Locale locale2 = Locale.US;
Locale locale3 = new US.Locale();
Locale locale4 = Locale("US");
Locale locale5 = new Locale(Locale.US);
Choose the correct option based on this code segment: String []exams = { "OCAJP 8", "OCPJP 8", "Upgrade to OCPJP 8" };
Predicate isOCPExam = exam -> exam.contains("OCP"); // LINE-1
List<String> ocpExams = Arrays.stream(exams)
.filter(exam -> exam.contains("OCP"))
.collect(Collectors.toList()); // LINE-2
boolean result =
ocpExams.stream().anyMatch(exam -> exam.contains("OCA")); // LINE-3
System.out.println(result);
This code results in a compiler error in line marked with the comment LINE-1
This code results in a compiler error in line marked with the comment LINE-2
This code results in a compiler error in line marked with the comment LINE-3
This program prints: true
This program prints: false
Which one of the following code snippets shows the correct usage of try-with-resources statement? a) public static void main(String []files) {
try (FileReader inputFile
= new FileReader(new File(files[0]))) {
//...
}
catch(IOException ioe) {}
}
b) public static void main(String []files) {
try (FileReader inputFile
= new FileReader(new File(files[0]))) {
//...
}
finally { }
catch(IOException ioe) {}
}
c) public static void main(String []files) {
try (FileReader inputFile
= new FileReader(new File(files[0]))) {
//...
}
catch(IOException ioe) {}
finally { inputFile.close(); }
}
d) public static void main(String []files) {
try (FileReader inputFile
= new FileReader(new File(files[0]))) {
//...
}
}
Two friends are waiting for some more friends to come so that they can go to a restaurant for dinner together. Which synchronization construct could be used here to programmatically simulate this situation? java.util.concurrent.RecursiveTask
java.util.concurrent.locks.Lock
java.util.concurrent.CyclicBarrier
java.util.concurrent.RecursiveAction
Choose the correct option based on this program: import java.util.*;
public class ResourceBundle_it_IT extends ListResourceBundle {
public Object[][] getContents() {
return contents;
}
static final Object[][] contents = {
{ "1", "Uno" },
{ "2", "Duo" },
{ "3", "Trie" },
};
public static void main(String args[]) {
ResourceBundle resBundle =
ResourceBundle.getBundle("ResourceBundle", new Locale("it", "IT", ""));
System.out.println(resBundle.getObject(new Integer(1).toString()));
}
}
This program prints the following: Uno
This program prints the following: 1
This program will throw a MissingResourceException
This program will throw a ClassCastException
Given this code segment: Set<String> set = new CopyOnWriteArraySet<String>(); // #1
set.add("2");
set.add("1");
Iterator<String> iter = set.iterator();
set.add("3");
set.add("-1");
while(iter.hasNext()) {
System.out.print(iter.next() + " ");
}
Choose the correct option based on this code segment . This code segment prints the following: 2 1 This code segment the following: 1 2 This code segment prints the following: -1 1 2 3 This code segment prints the following: 2 1 3 -1 This code segment throws a ConcurrentModificationException
This code segment results in a compiler error in statement #1 Choose the correct option based on this code segment: Stream<Integer> ints = Stream.of(1, 2, 3, 4);
boolean result = ints.parallel().map(Function.identity()).isParallel();
System.out.println(result);
This code segment results in compiler error(s) This code segment throws InvalidParallelizationException
for the call parallel()
This code segment prints: false
This code segment prints: true
Choose the correct option based on this code segment: Path currPath = Paths.get(".");
try (DirectoryStream<Path> javaFiles = Files.newDirectoryStream(currPath, "*.{java}")) {
for(Path javaFile : javaFiles) {
System.out.println(javaFile);
}
} catch (IOException ioe) {
System.err.println("IO Error occurred");
System.exit(-1);
}
This code segment throws a PatternSyntaxException
This code segment throws an UnsupportedOperationException
This code segment throws an InvalidArgumentException
This code segment lists the files ending with suffix .java
in the current directory Given this code segment: Path aFilePath = Paths.get("D:\\dir\\file.txt");
Iterator<Path> paths = aFilePath.iterator();
while(paths.hasNext()) {
System.out.print(paths.next() + " ");
}
Choose the correct option assuming that you are using a Windows machine and the file D:\dir\file.txt
does not exist in the underlying file system. The program throws a FileNotFoundException
The program throws an InvalidPathException
The program throws an UnsupportedOperationException
The program gets into an infinite loop and keeps printing: path element: dir
The program prints the following: dir file.txt
Which of the following is NOT a problem associated with thread synchronization using mutexes? Deadlock Lock starvation Type erasure Livelock Assume that a thread acquires a lock on an object obj; the same thread again attempts to acquire the lock on the same object obj. What will happen? If a thread attempts to acquire a lock again, it will result in throwing an IllegalMonitorStateException
If a thread attempts to acquire a lock again, it will result in throwing an AlreadyLockAcquiredException
It is okay for a thread to acquire lock on obj
again, and such an attempt will succeed If a thread attempts to acquire a lock again, it will result in a deadlock Which one of the following interfaces is empty (i.e., an interface that does not declare any methods)? java.lang.AutoCloseable
interface java.util.concurrent.Callable<T>
interface java.lang.Cloneable
interface java.lang.Comparator<T>
interface Consider the following program and choose the correct option that describes its output: import java.util.concurrent.atomic.AtomicInteger;
class Increment {
public static void main(String []args) {
AtomicInteger i = new AtomicInteger(0);
increment(i);
System.out.println(i);
}
static void increment(AtomicInteger atomicInt){
atomicInt.incrementAndGet();
}
}
0 1 This program throws an UnsafeIncrementException
This program throws a NonThreadContextException
What is the output of the following program? class EnumTest {
enum Directions { North, East, West, South };
enum Cards { Spade, Hearts, Club, Diamond };
public static void main(String []args) {
System.out.println("equals: " + Directions.East.equals(Cards.Hearts));
System.out.println("ordinals: " +
(Directions.East.ordinal() == Cards.Hearts.ordinal()));
}
}
equals: false
ordinals: false
equals: true
ordinals: false
equals: false
ordinals: true
equals: true
ordinals: true
Consider the following program and choose the correct option: import java.util.concurrent.atomic.AtomicInteger;
class AtomicVariableTest {
private static AtomicInteger counter = new AtomicInteger(0);
static class Decrementer extends Thread {
public void run() {
counter.decrementAndGet(); // #1
}
}
static class Incrementer extends Thread {
public void run() {
counter.incrementAndGet(); // #2
}
}
public static void main(String []args) {
for(int i = 0; i < 5; i++) {
new Incrementer().start();
new Decrementer().start();
}
System.out.println(counter);
}
}
This program will always print 0 This program will print any value between -5 to 5 If you make the run()
methods in the Incrementer
and Decrementer
classes synchronized, this program will always print 0 The program will report compilation errors at statements #1 and #2 Which one of the following statements will compile without any errors? Supplier<LocalDate> now = LocalDate::now();
Supplier<LocalDate> now = () -> LocalDate::now;
String now = LocalDate::now::toString;
Supplier<LocalDate> now = LocalDate::now;
For the following enumeration definition, which one of the following prints the value 2 in the console? enum Pets { Cat, Dog, Parrot, Chameleon };
System.out.print(Pets.Parrot.ordinal());
System.out.print(Pets.Parrot);
System.out.print(Pets.indexAt("Parrot"));
System.out.print(Pets.Parrot.value());
System.out.print(Pets.Parrot.getInteger());
Assume that the current directory is “D:\workspace\ch14-test”. Choose the correct option based on this code segment: Path testFilePath = Paths.get(".\\Test");
System.out.println("file name:" + testFilePath.getFileName());
System.out.println("absolute path:" + testFilePath.toAbsolutePath());
System.out.println("Normalized path:" + testFilePath.normalize());
file name:Test
absolute path:D:\workspace\ch14-test\.\Test
Normalized path:Test
file name:Test
absolute path:D:\workspace\ch14-test\Test
Normalized path:Test
file name:Test
absolute path:D:\workspace\ch14-test\.\Test
Normalized path:D:\workspace\ch14-test\.\Test
file name:Test
absolute path:D:\workspace\ch14-test\.\Test
Normalized path:D:\workspace\ch14-test\Test
Given this code segment: BufferedReader br = new BufferedReader(new FileReader("names.txt"));
System.out.println(br.readLine());
br.mark(100); // MARK
System.out.println(br.readLine());
br.reset(); // RESET
System.out.println(br.readLine());
Assume that names.txt
exists in the current directory, and opening the file succeeds, and br
points to a valid object. The content of the names.txt is the following: olivea
emma
margaret
emily
Choose the correct option. This code segment prints the following: olivea
emma
margaret
This code segment prints the following: olivea
emma
olivea
This code segment prints the following: olivea
emma
emma
This code segment throws an IllegalArgumentException
in the line MARK
This code segment throws a CannotResetToMarkPositionException
in the line RESET
Given this class definition : abstract class Base {
public abstract Number getValue();
}
Which of the following two options are correct concrete classes extending Base
class? class Deri extends Base {
protected Number getValue() {
return new Integer(10);
}
}
class Deri extends Base {
public Integer getValue() {
return new Integer(10);
}
}
class Deri extends Base {
public Float getValue(float flt) {
return new Float(flt);
}
}
class Deri extends Base {
public java.util.concurrent.atomic.AtomicInteger getValue() {
return new java.util.concurrent.atomic.AtomicInteger(10);
}
}
Which TWO of the following classes are defined in the java.util.concurrent.atomic
package? AtomicBoolean
AtomicDouble
AtomicReference<V>
AtomicString
AtomicObject<V>
Given the following class and interface definitions : class CannotFlyException extends Exception {}
interface Birdie {
public abstract void fly() throws CannotFlyException;
}
interface Biped {
public void walk();
}
abstract class NonFlyer {
public void fly() { System.out.print("cannot fly "); } // LINE A
}
class Penguin extends NonFlyer implements Birdie, Biped { // LINE B
public void walk() { System.out.print("walk "); }
}
Select the correct option for this code segment: Penguin pingu = new Penguin();
pingu.walk();
pingu.fly();
Compiler error in line with comment LINE A because fly()
does not declare to throw CannotFlyException
Compiler error in line with comment LINE B because fly()
is not defined and hence need to declare it abstract It crashes after throwing the exception CannotFlyException
When executed, the program prints “walk cannot fly” Given this class definition: class Outer {
static class Inner {
public final String text = "Inner";
}
}
Which one of the following expressions when replaced for the text in place of the comment /CODE HERE/ will print the output “Inner” in console? class InnerClassAccess {
public static void main(String []args) {
System.out.println(/*CODE HERE*/);
}
}
new Outer.Inner().text
Outer.new Inner().text
Outer.Inner.text
new Outer().Inner.text
Given this code snippet: String[] fileList = { "/file1.txt", "/subdir/file2.txt", "/file3.txt" };
for (String file : fileList) {
try {
new File(file).mkdirs();
}
catch (Exception e) {
System.out.println("file creation failed");
System.exit(-1);
}
}
Assume that the underlying file system has the necessary permissions to create files, and that the program executed successfully without printing the message “file creation failed.” (In the answers, note that the term “current directory” means the directory from which you execute this program, and the term “root directory” in Windows OS means the root path of the current drive from which you execute this program.) Choose the correct option: This code segment will create file1.txt and file3.txt files in the current directory, and file2.txt file in the subdir directory of the current directory This code segment will create file1.txt and file3.txt directories in the current directory and the file2.txt directory in the “subdir” directory in the current directory This code segment will create file1.txt and file3.txt files in the root directory, and a file2.txt file in the “subdir” directory in the root directory This code segment will create file1.txt and file3.txt directories in the root directory, and a file2.txt directory in the “subdir” directory in the root directory Given these class definitions: class Book {
public void read() {
System.out.println("read!");
}
}
public class BookUse {
// DEFINE READBOOK HERE
public static void main(String []args) {
new BookUse().readBook(Book::new);
}
}
Which one of the following code segments when replaced with the comment “DEFINE READBOOK HERE” inside the BookUse class will result in printing “read!” on the console? private void readBook(Supplier<? extends Book> book) {
book.get().read();
}
private static void readBook(Supplier<? extends Book> book) {
Book::read;
}
private void readBook(Consumer<? extends Book> book) {
book.accept();
}
private void readBook(Function<? extends Book> book) {
book.apply(Book::read);
}
Given the class definition: class Employee {
String firstName;
String lastName;
public Employee (String fName, String lName) {
firstName = fName;
lastName = lName;
}
public String toString() { return firstName + " " + lastName; }
String getFirstName() { return firstName; }
String getLastName() { return lastName; }
}
Here is a code segment: Employee[] employees = { new Employee("Dan", "Abrams"),
new Employee("Steve", "Nash"),
new Employee("John", "Nash"),
new Employee("Dan", "Lennon"),
new Employee("Steve", "Lennon")
};
Comparator<Employee> sortByFirstName =
((e1, e2) -> e1.getFirstName().compareTo(e2.getFirstName()));
Comparator<Employee> sortByLastName =
((e1, e2) -> e1.getLastName().compareTo(e2.getLastName()));
// SORT
The sorting needs to be performed in descending order of the first names; when first names are the same, the names should then be sorted in ascending order of the last names. For that, which one of the following code segment will you replace for the line marked by the comment SORT
? Stream.of(employees)
.sorted(sortByFirstName.thenComparing(sortByLastName))
.forEach(System.out::println);
Stream.of(employees)
.sorted(sortByFirstName.reversed().thenComparing(sortByLastName))
.forEach(System.out::println);
Stream.of(employees)
.sorted(sortByFirstName.thenComparing(sortByLastName).reversed())
.forEach(System.out::println);
Stream.of(employees)
.sorted(sortByFirstName.reversed().thenComparing(sortByLastName).reversed())
.forEach(System.out::println);
Given this code snippet: Statement statement = connection.createStatement
(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet resultSet = statement.executeQuery
("SELECT * FROM EMPLOYEE WHERE EMPNAME = \"John\"");
resultSet.updateString("EMPNAME", "Jonathan");
// UPDATE
Assume that the variable connection points to a valid Connection
object and there exists an employee record with EMPNAME
value “John”. The resultSet
is updated by changing the value of EMPNAME
column with the value “Jonathan” instead of “John”. For this change to be reflected in the underlying database, which one of the following statements will you replace with the comment UPDATE
? connection.updateAllResultSets();
resultSet.updateRow();
statement.updateDB();
connection.updateDatabase();
Given these class definitions : class ReadDevice implements AutoCloseable {
public void read() throws Exception {
System.out.print("read; ");
throw new Exception();
}
public void close() throws Exception {
System.out.print("closing ReadDevice; ");
}
}
class WriteDevice implements AutoCloseable {
public void write() {
System.out.print("write; ");
}
public void close() throws Exception {
System.out.print("closing WriteDevice; ");
}
}
What will this code segment print? try(ReadDevice rd = new ReadDevice();
WriteDevice wd = new WriteDevice()) {
rd.read();
wd.write();
} catch(Exception e) {
System.out.print("Caught exception; ");
}
read; closing WriteDevice; closing ReadDevice; Caught exception;
read; write; closing WriteDevice; closing ReadDevice; Caught exception;
read; write; closing ReadDevice; closing WriteDevice; Caught exception;
read; write; Caught exception; closing ReadDevice; closing WriteDevice;
read; Caught exception; closing ReadDevice; closing WriteDevice;
Select all the statements that are true about streams (supported in java.util.stream.Stream
interface)? Computation on source data is performed in a stream only when the terminal operation is initiated, i.e., streams are “lazy” Once a terminal operation is invoked on a stream, it is considered consumed and cannot be used again Once a stream is created as a sequential stream, its execution mode cannot be changed to parallel stream (and vice versa) If the stream source is modified when the computation in the stream is being performed, then it may result in unpredictable or erroneous results Given the code segment: List<Integer> integers = Arrays.asList(15, 5, 10, 20, 25, 0);
// GETMAX
Which of the code segments can be replaced for the comment marked with GETMAX to return the maximum value? Integer max = integers.stream().max((i, j) -> i - j).get();
Integer max = integers.stream().max().get();
Integer max = integers.max();
Integer max = integers.stream().mapToInt(i -> i).max();
Given the class definition : class NullableBook {
Optional<String> bookName;
public NullableBook(Optional<String> name) {
bookName = name;
}
public Optional<String> getName() {
return bookName;
}
}
Choose the correct option based on this code segment: NullableBook nullBook = new NullableBook(Optional.ofNullable(null));
Optional<String> name = nullBook.getName();
name.ifPresent(System.out::println).orElse("Empty"); // NULL
This code segment will crash by throwing NullPointerException
This code segment will print: Empty
This code segment will print: null
This code segment will result in a compiler error in line marked with NULL
Choose the correct option for this code segment : List<String> lines = Arrays.asList("foo;bar;baz", "", "qux;norf");
lines.stream()
.flatMap(line -> Arrays.stream(line.split(";"))) // FLAT
.forEach(str -> System.out.print(str + ":"));
This code will result in a compiler error in line marked with the comment FLAT
This code will throw a java.lang.NullPointerException
This code will throw a java.util.regex.PatternSyntaxException
This code will print foo:bar:baz::qux:norf:
Choose the correct option based on this code segment: LocalDate feb28th = LocalDate.of(2015, Month.FEBRUARY, 28);
System.out.println(feb28th.plusDays(1));
This program prints: 2015-02-29
This program prints: 2015-03-01
This program throws a java.time.DateTimeException
This program throws a java.time.temporal.UnsupportedTemporalTypeException
Choose the correct option based on this code segment : List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
ints.removeIf(i -> (i % 2 ==0)); // LINE
System.out.println(ints);
This code segment prints: [1, 3, 5]
This code segment prints: [2, 4]
This code segment prints: [1, 2, 3, 4, 5]
This code segment throws java.lang.UnsupportedOperationException
This code segment results in a compiler error in the line marked with the comment LINE
Given the class definition: class Point {
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// other methods elided
}
Which one of the following enforces encapsulation? (Select all that apply.) Make data members x
and y
private Make the Point
class public Make the constructor of the Point
class private Remove the getter methods getX()
and getY()
methods from the Point
class Make the Point
class static Given the definition: class Sum implements Callable<Long> { // LINE_DEF
long n;
public Sum(long n) {
this.n = n;
}
public Long call() throws Exception {
long sum = 0;
for(long longVal = 1; longVal <= n; longVal++) {
sum += longVal;
}
return sum;
}
}
Given that the sum of 1 to 5 is 15, select the correct option for this code segment: Callable<Long> task = new Sum(5);
ExecutorService es = Executors.newSingleThreadExecutor(); // LINE_FACTORY
Future<Long> future = es.submit(task); // LINE_CALL
System.out.printf("sum of 1 to 5 is %d", future.get());
es.shutdown();
This code results in a compiler error in the line marked with the comment LINE_DEF
This code results in a compiler error in the line marked with the comment LINE_FACTORY
This code results in a compiler error in the line marked with the comment LINE_CALL
This code prints: sum of 1 to 5 is 15
Given this class definition: public class AssertCheck {
public static void main(String[] args) {
int score = 0;
int num = 0;
assert ++num > 0 : "failed";
int res = score / num;
System.out.println(res);
}
}
Choose the correct option assuming that this program is invoked as follows: java –ea AssertCheck
This program crashes by throwing java.lang.AssertionError
with the message “failed
” This program crashes by throwing java.lang.ArithmeticException
with the message “/ by zero
” This program prints: 0
This program prints “failed
” and terminates normally Given this code segment: BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String integer = br.readLine();
// CODE
System.out.println(val);
Which one of the following statements when replaced by the comment CODE will successfully read an integer value from console? int val = integer.getInteger();
int val = Integer.parseInt(integer);
int val = String.parseInt(integer);
int val = Number.parseInteger(integer);
Which one of the following definitions of the AResource
class implementation is correct so that it can be used with try-with-resources statement? a) class AResource implements Closeable {
protected void close() /* throws IOException */ {
// body of close to release the resource
}
}
b) class AResource implements Closeable {
public void autoClose() /* throws IOException */ {
// body of close to release the resource
}
}
c) class AResource implements AutoCloseable {
void close() /* throws IOException */ {
// body of close to release the resource
}
}
d) class AResource implements AutoCloseable {
public void close() throws IOException {
// body of close to release the resource
}
}
Which of the following are functional interfaces ? (Select all that apply.) a) @FunctionalInterface
interface Foo {
void execute();
}
b) @FunctionalInterface
interface Foo {
void execute();
boolean equals(Object arg0);
}
c) @FunctionalInterface
interface Foo {
boolean equals(Object arg0);
}
d) interface Foo{}
Choose the correct option based on this code segment : Stream<String> words = Stream.of("eeny", "meeny", "miny", "mo"); // LINE_ONE
String boxedString = words.collect(Collectors.joining(", ", "[", "]")); // LINE_TWO
System.out.println(boxedString);
This code results in a compiler error in line marked with the comment LINE_ONE
This code results in a compiler error in line marked with the comment LINE_TWO
This program prints: [eeny, meeny, miny, mo]
This program prints: [eeny], [meeny], [miny], [mo]
Choose the correct option based on the following code snippet. Assume that DbConnector.connectToDb()
returns a valid Connection
object and that the EMPLOYEE
table has a column named CUSTOMERID
of type VARCHAR(3)
. ResultSet resultSet = null;
try (Connection connection = DbConnector.connectToDb()) { // LINE_ONE
Statement statement = connection.createStatement();
resultSet = statement.executeQuery
("SELECT * FROM CUSTOMER WHERE CUSTOMERID = 1212"); // LINE_TWO
}
while (resultSet.next()){ // LINE_THREE
resultSet.getString("CUSTOMERID");
}
This code results in a compiler error in line marked with the comment LINE_ONE
This code results in a compiler error in line marked with the comment LINE_TWO
This code results in a compiler error in line marked with the comment LINE_THREE
This code prints "1212"
on the console and terminates This code gets into an infinite loop and keeps printing "1212"
on the console This code throws SQLException
Given this code snippet: public static Connection connectToDb() throws SQLException {
String url = "jdbc:mysql://localhost:3306/";
String database = "addressBook";
String userName = "root";
String password = "mysql123";
// CONNECT_TO_DB
}
Which one of the following statements will you replace with the comment CONNECT_TO_DB
to create a Connection
object? return DatabaseManager.getConnection(url, database, userName, password);
return Connection.getConnection(url, database, userName, password);
return DriverManager.getConnection(url + database, userName, password);
return DatabaseDriver.getConnection(url + database, userName, password);
Choose the correct option based on this code segment: Path path = Paths.get("file.txt");
// READ_FILE
lines.forEach(System.out::println);
Assume that a file named “file.txt” exists in the directory in which this code segment is run and has the content “hello”. Which one of these options can be replaced by the text READ_FILE that will successfully read the “file.txt” and print “hello” on the console? List<String> lines = Files.lines(path);
Stream<String> lines = Files.lines(path);
Stream<String> lines = File.readLines(path);
Stream<String> lines = Files.readAllLines(path);
答案纸
问题编号 | 回答 | 问题编号 | 回答 | 问题编号 | 回答 |
---|---|---|---|---|---|
one | Thirty-one | Sixty-one | |||
Two | Thirty-two | Sixty-two | |||
three | Thirty-three | Sixty-three | |||
four | Thirty-four | Sixty-four | |||
five | Thirty-five | Sixty-five | |||
six | Thirty-six | Sixty-six | |||
seven | Thirty-seven | Sixty-seven | |||
eight | Thirty-eight | sixty-eight | |||
nine | Thirty-nine | sixty-nine | |||
Ten | Forty | Seventy | |||
Eleven | Forty-one | Seventy-one | |||
Twelve | forty-two | seventy-two | |||
Thirteen | Forty-three | Seventy-three | |||
Fourteen | forty-four | Seventy-four | |||
Fifteen | Forty-five | Seventy-five | |||
Sixteen | Forty-six | Seventy-six | |||
Seventeen | Forty-seven | Seventy-seven | |||
Eighteen | Forty-eight | seventy-eight | |||
Nineteen | forty-nine | Seventy-nine | |||
Twenty | Fifty | Eighty | |||
Twenty-one | Fifty-one | Eighty-one | |||
Twenty-two | fifty-two | Eighty-two | |||
Twenty-three | Fifty-three | Eighty-three | |||
Twenty-four | Fifty-four | Eighty-four | |||
Twenty-five | Fifty-five | eighty-five | |||
Twenty-six | fifty-six | ||||
Twenty-seven | Fifty-seven | ||||
Twenty-eight | Fifty-eight | ||||
Twenty-nine | Fifty-nine | ||||
Thirty | Sixty |
答案和解释
d) This code segment does not print anything on the console The limit()
method is an intermediate operation and not a terminal operation. Since there is no terminal operation in this code segment, elements are not processed in the stream and hence it does not print anything on the console. b) while( (ch = inputFile.read()) != -1) {
The read()
method returns -1 when the file reaches the end. Why other options are wrong: Option a) Since ch
is of type int
, it cannot be compared with null
. Option c) With the check != 0, the program will never terminate since inputFile.read()
returns -1 when it reaches end of the file. Option d) Using the identifier EOF
will result in a compiler error. a) Base
Derived
DeriDerived
Whenever a class gets instantiated, the constructor of its base classes (the constructor of the root of the hierarchy gets executed first) gets invoked before the constructor of the instantiated class. d) This code prints: Awaiting
Awaiting
Awaiting
Let's play
There are three threads expected in the CyclicBarrier
because of the value 3 given as the first argument to the CyclicBarrier
constructor. When a thread executes, it prints “Awaiting
” and awaits for the other threads (if any) to join. Once all three threads join, they cross the barrier and the message “Let's play
” gets printed on the console. c) private Point() {
this(0, 0);
}
Options a) and b) Both the calls super()
and this()
cannot be provided in a constructor Option d) The call this();
will result in a recursive constructor invocation for Point()
constructor (and hence the compiler will issue an error) Option e) You cannot refer to an instance field x
while explicitly invoking a constructor using this
keyword b) Removing Stmt-1
will make the program compilable and it will print the following: Base: Hello Derived. c) Removing Stmt-2
will make the program compilable and it will print the following: Base Derived
d) Removing both Stmt-1
and Stmt-2
will make the program compilable and it will print the following: Base Derived
Why other options are wrong: Option a) If you remove Stmt
-1, a call to super
(s) will result in printing Base: Hello
, and then constructor of the Derived
class invocation will print Derived
. Hence it does not print: Base Derived
. Option e) If you remove Stmt-1
and Stmt-2
, you will get a compilable program but it will result in printing: Base Derived
and not Base: Hello Derived
. a) The compiler will report an error at statement line marked with the comment #1 Statement #1 will result in a compiler error since the keyword protected
is not allowed inside a method body. You cannot provide access specifiers (public
, protected
, or private
) inside a method body. Why other options are wrong: Option b) It is acceptable to extend a base class and hence there is no compiler error in line marked with comment #2. Option c) It is acceptable to pass null to printf
function hence there is no compiler error in line marked with comment #2. Option d) This program will not compile cleanly and hence this option is wrong. c) int years = Period.between(joiningDate, now).getYears();
The between()
method in Period
returns a Period
object. The getYears()
method called on the returned Period
returns an int
. Hence, option c) that declares years as int
is the correct option. Using the other three options will result in compiler errors because the getYears()
method of Period
return an int
. d) Outer.Inner inner = new Outer().new Inner();
Option d) uses the correct syntax for instantiating Outer and Inner classes. The other three options will result in compiler error(s). c) This program runs and prints 10 An inner class can access even the private members of the outer class. Similarly, the private variable belonging to the inner class can be accessed in the outer class. Why other options are wrong: Options a) and b) are wrong because this program compiles without any errors. The variable mem
is initialized to value 10 and that gets printed by the program (and not 0) and hence Option d) is wrong. e) When executed, this program prints the following: yes, instance of AnEnum
yes, instance of EnumBase
yes, instance of Enum
An enumeration can implement an interface (but cannot extend a class, or cannot be a base class). Each enumeration constant is an object of its enumeration type. An enumeration automatically extends the abstract class java.util.Enum
. Hence, all the three instanceof
checks succeed. Why other options are wrong: This program compiles cleanly and hence options a) and b) are wrong. Options c) and d) do not provide the complete output of the program and hence they are also incorrect. a) An enum
can have private
constructor c) An enum
can have public
methods and fields d) An enum
can implement an interface Why other options are wrong: Option b) An enum
cannot have public
constructor(s) Option e) An enum
cannot extend a class c) The program will report a compilation error at statement marked with the comment #3 Statements marked with the comment #1 and #2 will not result in any compiler errors; only access to the variable var
will generate a compiler error since the access is ambiguous (since the variable is declared in both base1
and base2
). c) The line marked with comment THREE
will result in a compiler error Options a) and b) For the substitution to succeed, the type substituted for the wildcard ?
should be DI
or one of its super types. Option c) The type DDI
is not a super type of DI
, so it results in a compiler error. Option d) The type argument is not provided, meaning that C is a raw type in the expression new C()
. Hence, this will elicit a compiler warning, but not an error. c) class X <T extends DI> { }
The keyword extends
is used to specify the upper bound for type T
; with this, only the classes or interfaces implementing the interface DI
can be used as a replacement for T
. Note that the extends
keyword is used for any base type—irrespective of whether the base type is a class or an interface. c) A Factory
class may use Singleton
pattern A Factory
class generates the desired type of objects on demand. Hence, it might be required that only one Factory
object exists; in this case, Singleton
can be employed in a Factory
class. Why other options are wrong: a) A Singleton
class needs to have a static member to return a singleton instance b) A Singleton
class must declare its constructor(s) private to ensure that they are not instantiated d) A static method (typically named getInstance()
) with public access may need to be provided to get the instance of the Singleton
class. b) Class Test
is related with ClassC
with a composition relationship. When a class inherits from another class, they share an IS-A relationship. On the other hand, if a class uses another class (by declaring an instance of another class), then the first class has a HAS-A relationship with the used class. c) The program prints the following: Brazil China India Russia. For the sort()
method, null
value is passed as the second argument, which indicates that the elements’ “natural ordering” should be used. In this case, natural ordering for Strings
results in the strings sorted in ascending order. Note that passing null
to the sort()
method does not result in a NullPointerException
. The statement marked with COMPARE_TO
will compile without errors. Note that the variable comparer
is unused in this code segment. b) class Q<T> {
T mem;
public Q(T arg) {
mem = arg;
}
}
Option a) You cannot make a static reference of type T
in a generic class. Option c) and d) You cannot instantiate the type T
or T[]
using new
operator in a generic class. b) public Object[][] getContents() {
return new Object[][] { { "1", "Uno" }, { "2", "Duo" }, { "3", "Trie" }};
}
The getContents()
method is declared in ListResourceBundle
as follows: protected abstract Object[][] getContents()
The other three definitions are incorrect overrides and will result in compiler error(s). a) Iterable<T>
The interface Iterable<T>
declares this single method: Iterator<T> iterator();
This iterator
() method returns an object of type Iterator
Iterable
for-each
loop. Though Iterable
interface (in Java 8) defines forEach()
and spliterator()
methods, they are default methods and not static methods. Why other options are wrong: Option b) The Iterator<T>
interface declares abstract methods hasNext()
and next()
, and defines default methods remove()
and forEachRemaining()
. Option c) The Enumeration<T>
interface declares hasMoreElements() and nextElement()
methods. Option d) There is no interface named ForEach<T>
in the Java core library. c) This program prints: 11 This program compiles without any errors. The variable words point to a stream of String
s. The call mapToInt(String::length)
results in a stream of Integer
s with the length of the strings. One of the overloaded versions of reduce()
method takes two arguments: T reduce(T identity, BinaryOperator<T> accumulator);
The first argument is the identity
value, which is given as the value 0 here. The second operand is a BinaryOperator
match for the lambda expression (len1, len2) -> len1 + len2
. The reduce()
method thus adds the length of all the three strings in the stream, which results in the value 11. b) Using java.util.concurrent.ThreadLocalRandom
java.lang.Math.random()
is not efficient for concurrent programs. Using ThreadLocalRandom
results in less overhead and contention when compared to using Random
objects in concurrent programs (and hence using this class type is the best option in this case). java.util.RandomAccess
is unrelated to random number generation. This interface is the base interface for random access data structures and is implemented by classes such as Vector
and ArrayList. java.lang.ThreadLocal<T>
class provides support for creating thread-local variables. d) LocalDate firstOct2015 = LocalDate.parse("01/10/2015", fromDateFormat);
You need to use LocalDate
for parsing the date string given in the DateTimeFormatter
variable fromDateFormat
(with the format string MM/dd/yyyy
”). Other options will not compile. b) The definition of asList2
will result in a compiler error. In the asList2
method definition, temp
is declared as ArrayList
>. Since the template type is a wild-card, you cannot put any element (or modify the container). Hence, the method call temp.add(element);
will result in a compiler error. a) Integer apply = func.apply(10).apply(20);
The IntFunction<R>
takes an argument of type int
and returns a value of type R.
The UnaryOperator<T>
takes an argument of type T
and returns a value of type T
. The correct way to invoke func
is to call func.apply(10).apply(10)
(the other three options do not compile). The first call apply(10)
results in an Integer
object that is passed to the lambda expression; calling apply(20)
results in executing the expression (i * j)
that evaluates to 200. The other three options will result in compiler error(s). e) When run, this program will print the following: null {} {}
The lines marked with comments ADD_MAP
and ADD_HASHMAP
are valid uses of the diamond operator to infer type arguments. Calling the add()
method passing null
does not result in a NullPointerException
. The program, when run, will successfully print the output null {} {}
(null output indicates a null value was added to the list, and the {} output indicates that Map
is empty). c) This code will print: Happy birthday!
This code gets the month-and-day components from the given LocalDate
and creates a MonthDay
object. Another way to create a MonthDay
object is to call the from()
method and pass a LocalDate
object. The equals()
method compares if the month and date components are equal and if so returns true. Since the month and day components are equal in this code (assuming that the today’s date is 4th November 2015 as given in the question), it results in printing “Happy birthday!
”. a) Base<Number> b = new Base<Number>();
f) Derived<Integer> b = new Derived<Integer>();
Note that Base
and Derived
are not related by an inheritance relationship. Further, for generic type parameters, subtyping doesn’t work: you cannot assign a derived generic type parameter to a base type parameter. b) AtomicInteger
c) AtomicLong
Classes AtomicInteger
and AtomicLong
extend Number
class. Why other options are wrong: Option a) AtomicBoolean
does not extend java.lang.Number
. Options d) and e) Classes named as AtomicFloat
or AtomicDouble
do not exist in the java.util.concurrent.atomic
package. b) This program prints the following: false
Since methods equals()
and hashcode()
methods are not overridden in the Student
class, the contains()
method will not work as intended and prints false. a) ResourceBundle
is the base class and is an abstraction of resource bundles that contain locale-specific objects b) java.util.PropertyResourceBundle
is a concrete subclass of java.util.ResourceBundle
that manages resources for a locale using strings provided in the form of a property file d) java.util.ListResourceBundle
defines the getKeys()
method that returns enumeration of keys contained in the resource bundle The option c) is not to be selected. There is no such method named getContents()
method that has the return type Object [][]
. It has the method getKeys()
that returns an enumeration of keys contained in the resource bundle. It is classes that extend java.util.ListResourceBundle
(and not java.util.PropertyResourceBundle
as given in this option) that must override the getContents()
method that has the return type Object [][]
. a) TheExecutor
interface declares a single method execute(Runnable command)
that executes the given command at some time in the future b) The Callable
interface declares a single method call()
that computes a result d) The CyclicBarrier
class allows threads to wait for each other to reach a common barrier point These three options are true. Option c) is incorrect because the CopyOnWriteArrayList
class is thread-safe whereas ArrayList
class is not thread-safe. c) This code segment prints the following output: In AutoCloseableImpl.close()
In CloseableImpl.close()
The types implementing AutoCloseable
can be used with a try-with-resources statement. The Closeable
interface extends AutoCloseable
, so classes implementing Closeable
can also be used with a try-with-resources statement. The close()
methods are called in the opposite order when compared to the order of resources acquired in the try-with-resources statement. So, this program calls the close()
method of AutoCloseableImpl
first, and after that calls the close()
method on the CloseableImpl
object. b) This program prints: [1, 4, 9, 16, 25]
The replaceAll()
method (added in Java 8 to the List
interface) takes an UnaryOperator
as the argument. In this case, the unary operator squares the integer values. Hence, the program prints [1, 4, 9, 16, 25]. The underlying List
object returned by Arrays.asList()
method can be modified using the replaceAll()
method and it does not result in throwing java.lang.UnsupportedOperationException
. d) The compiler will report an error at the statement marked with the comment #3 Both of the specified exceptions belong to the same hierarchy (FileNotFoundException
derives from an IOException
), so you cannot specify both exceptions together in the multi-catch handler block. It is not a compiler error to explicitly call close()
method for a FileReader
object inside a try-with-resources block. d) When executed, this program does not print any output and terminates normally The program compiles cleanly without any errors. Assertions are disabled by default. Since assertions are not enabled when invoking this program, it does not evaluate the assert
expression. Hence, the program terminates normally without printing any output on the console. a) -1 The read()
method returns the value -1 if end-of-stream (EOS) is reached, which is checked in this while
loop. b) The program will result in creating the file World.txt
with the contents “World!” in it. The method call skip(n)
skips n
bytes (i.e., moves the buffer pointer by n
bytes). In this case, 6 bytes need to be skipped, so the string “Hello” is not copied in the while
loop while reading and writing the file contents. Why other options are wrong: Option a) The skip()
method can be called before the read()
method. Option c) No exception named CannotSkipException
exists. Option d) The skip()
method will throw an IllegalArgumentException
only if a negative value is passed. d) This program works fine and copies srcFile
to dstFile
Why other options are wrong: Options a) and b) This program does not get into an infinite loop because the condition check for end-of-stream (checking != -1)
is correct and the variable ch
needs to be declared as int
(and not char
). Option c) You can use ; (semi-colon) as separator for opening multiple resources in try-with-resources statement. b) InterfaceTwo<LocalDateTime> val = LocalDateTime::now;
The method now()
in LocalDateTime
is declared with the signature: LocalDateTime now()
The matching functional interface should also have an abstract method that takes no argument and returns a value of type T
. Since InterfaceTwo
has the abstract method declared as T foo()
, the statement InterfaceTwo<LocalDateTime> val = LocalDateTime::now;
succeeds. From the interface, the method can be invoked with val.foo();
since val
refers to LocalDateTime::now
, and it is equivalent to making the call LocalDateTime.now()
. b) Locale locale2 = Locale.US;
The static public final Locale US
member in the Locale
class is accessed using the expression Locale.US
, as in option b). The other options will result in compiler error(s). a) This code results in a compiler error in line marked with the comment LINE-1
The functional interface Predicate<T>
takes type T
as the generic parameter that is not specified in LINE-1
. This results in a compiler error because the lambda expression uses the method contains()
in the call exam.contains(“OCP”)
. If Predicate<String>
were specified (as in Predicate isOCPExam = exam -> exam.contains("OCP")
), this code segment would compile without errors, and when executed will print “false”. a) public static void main(String []files) {
try (FileReader inputFile
= new FileReader(new File(files[0]))) {
//...
}
catch(IOException ioe) {}
}
Why other options are wrong:
- 选项 b)在 catch 块之前提供 finally,这将导致一个编译器错误。
- 选项 c)在语句
inputFile.close()
中使用了在 finally 块中不可访问的变量inputFile
,从而导致编译器错误。选项 d)该上下文中所需的catch
块在代码中缺失(因为 try 块代码可能会抛出IOException
),因此是不正确的用法。
c) java.util.concurrent.CyclicBarrier
CyclicBarrier
is used when threads may need to wait at a predefined execution point until all other threads reach that point. This construct matches the given requirements. Why other options are wrong:
- 选项 a)和 d)
java.util.concurrent.RecursiveTask
和java.util.concurrent.RecursiveAction
在 fork-join 框架中执行任务的上下文中使用。 - 选项 b)
java.util.concurrent.locks.Lock
类为锁定和解锁提供了比使用synchronized
关键字更好的抽象。
a) This program prints the following: Uno
This program correctly extends ListResourceBundle
and defines a resource bundle for the locale it_IT
. The getObject()
method takes String
as an argument; this method returns the value of the matching key. The expression new Integer(1).toString()
is equivalent of providing the key “1
”, so the program prints Uno
in the console. a) This code segment prints the following: 2 1 This code segment modifies the underlying CopyOnWriteArrayList
container object using the add()
method. After adding the elements “2” and “1”, the iterator object is created. After creating this iterator object, two more elements are added, so internally a copy of the underlying container is created due to this modification to the container. But the iterator still refers to the original container that had two elements. So, this program results in printing 2 and 1. If a new iterator is created after adding these four elements, it will iterate over all those four elements. d) This code segment prints: true
The stream pointed by ints is a sequential stream because sequential is the default execution mode. The call to parallel()
method changes the execution mode to parallel stream. The isParallel()
method returns true because the current execution mode of the stream is parallel. Why other options are wrong: Option a) This code compiles without errors. The call to map(Function.identity())
is acceptable because the argument Function.identity()
just returns the same stream element it is passed with. Option b) It is possible to change the execution mode of a stream after it is created, and it does not result in throwing any exceptions. Option c) The isParallel()
method returns the current execution mode and not the execution mode when the stream was created. So the isParallel()
method returns true in this code (and not false as given in this option). d) This code segment lists the files ending with suffix .java
in the current directory The path “.” specifies the current directory. The pattern “*.{java}"
matches file names with suffix .java
. e) This code segment prints the following: dir file.txt
The name elements in a path
object are identified based on the separators. Note: To iterate name elements of the Path
object does not actually require that the corresponding files/directories must exist, so it will not result in throwing any exceptions. c) Type erasure Deadlocks, lock starvation, and livelocks are problems that arise when using mutexes for thread synchronization. Type erasure is a concept related to generics where the generic type information is lost once the generic type is compiled. c) It is okay for a thread to acquire lock on obj
again, and such an attempt will succeed Java locks are reentrant: a Java thread, if it has already acquired a lock, can acquire it again, and such an attempt will succeed. No exception is thrown and no deadlock occurs for this case. c) java.lang.Cloneable
interface From the documentation of clone()
method: “By convention, classes that implement this interface should override the Object.clone()
method. Note that this interface does not contain the clone
method.” Why other options are wrong:
- 选项 a)
AutoCloseable
接口声明了close()
方法。 - 选项 b)
Callable
声明call()
方法。 - 选项 d)
Comparator<T>
接口声明了compare()
和equals()
方法。
b) 1
The call atomicInt.incrementAndGet();
mutates the integer value passed through the reference variable atomicInt
, so the changed value is printed in the main()
method. Note that AtomicInteger
can be used in thread or non-thread context though it is not of any practical use when used in single-threaded programs. c) equals: false
ordinals: true
The equals()
method returns true only if the enumeration constants are the same. In this case, the enumeration constants belong to different enumerations, so the equals()
method returns false
. However, the ordinal values of the enumeration constants are equal since both are second elements in their respective enumerations. b) This program will print any value between −5 to 5 You have employed AtomicInteger
, which provides a set of atomic
methods such as incrementAndGet()
and decrementAndGet()
. Hence, you will always get 0 as the final value of counter. However, depending on thread scheduling, the intermediate counter values may be anywhere between −5 to +5, Hence the output of the program can range between −5 and +5. d) Supplier<LocalDate> now = LocalDate::now;
The now()
method defined in LocalDate
does not take any arguments and returns a LocalDate
object. Hence, the signature of now()
method matches that of the only abstract method in the Supplier
interface: T get()
. Hence, the method reference Local::now
can be assigned to Supplier<LocalDate>
and the statement compiles without any errors. Other options show improper use of method reference and they will result in compiler error(s). a) System.out.print(Pets.Parrot.ordinal());
The ordinal()
method prints the position of the enumeration constant within an enumeration and hence it prints 2 for this program. Why other options are wrong:
- 选项 b)调用
print(Pets.Parrot);
将字符串“Parrot”打印到控制台 - 选项 c)、d)和 e)在
Enum
中没有名为indexAt()
、value()
或getInteger()
的方法
a) file name:Test
absolute path:D:\workspace\ch14-test\.\Test
Normalized path:Test
The absolute path adds the path from the root directory; however, it does not normalize the path. Hence, “.\
” will be retained in the resultant path. On the other hand, the normalize
() method normalizes the path but does not make it absolute. c) This code segment prints the following: olivea
emma
emma
The method void mark(int limit)
in BufferedReader
marks the current position for resetting the stream to the marked position. The argument limit specifies the number of characters that may be read while still preserving the mark. This code segment marks the position after “olivea
” is read, so after reading “emma
,” when the marker is reset and the line is read again, it reads “emma
” once again. b) class Deri extends Base {
public Integer getValue() {
return new Integer(10);
}
}
d) class Deri extends Base {
public java.util.concurrent.atomic.AtomicInteger getValue() {
return new java.util.concurrent.atomic.AtomicInteger(10);
}
}
Option b) makes use of a co-variant return type (note that Integer
extends Number
), and defines the overriding method correctly. Option d) makes use of co-variant return type (note that AtomicInteger
extends Number
), and defines the overriding method correctly. Why the other two options are wrong:
- 选项 a)试图通过在基方法为公共方法时声明方法受保护来分配较弱的访问权限,因此是不正确的(导致编译器错误)。
- 在选项 c)中,方法
Float getValue(float flt)
没有覆盖Base
中的getValue
()方法,因为签名不匹配,所以它是不正确的(导致编译器错误)。
a) AtomicBoolean
and c) AtomicReference<V>
The class AtomicBoolean
supports atomically updatable Boolean values. The class AtomicReference
V
. Classes AtomicDouble
, AtomicString
, and AtomicObject
are not part of the java.util.concurrent.atomic
package. d) When executed, the program prints “walk cannot fly” In order to override a method, it is not necessary for the overridden method to specify an exception. However, if the exception is specified, then the specified exception must be the same or a subclass of the specified exception in the method defined in the super class (or interface). a) new Outer.Inner().text
The correct way to access fields of the static inner class is to use the inner class instance along with the outer class, so new Outer.Inner().text
will do the job. d) This code segment will create file1.txt and file3.txt directories in the root directory, and a file2.txt directory in the “subdir” directory in the root directory. The mkdirs()
method creates a directory for the given name. Since the file names have / in them, the method creates directories in the root directory (or root path for the Windows drive based on the path in which you execute this program). a) private void readBook(Supplier<? extends Book> book) {
book.get().read();
}
The Supplier<T>
interface declares the abstract method get()
. The get()
method does not take any arguments and returns an object of type T
. Hence, the call book.get().read()
succeeds and prints “read!” on the console. Why other options are wrong: Option b) Method references can be used in places where lambda expressions can be used. Hence, this code segment will result in a compiler error. Option c) The accept()
method in the Consumer<T>
interface requires an argument to be passed – since it is missing here, it will result in a compiler error. Option d) The Function<T, R>
interface takes two type parameters and hence this method definition will result in a compiler error. b) Stream.of(employees)
.sorted(sortByFirstName.reversed().thenComparing(sortByLastName))
.forEach(System.out::println);
The sortByFirstName
is a Comparator
that sorts names by the Employee
’s first name. Because we need to sort the names in descending order, we need to call the reversed()
method. After that, we need to sort the last names in ascending order, and hence we can call thenComparing(sortByLastName)
. b) resultSet.updateRow()
;
The call updateRow()
on the ResultSet
object updates the database. Other options will not compile. a) read; closing WriteDevice; closing ReadDevice; Caught exception;
The read()
method of ReadDevice
throws an exception, and hence the write()
method of WriteDevice
is not called. The try-with-resources statement releases the resources in the reverse order from which they were acquired. Hence, the close()
for WriteDevice
is called first, followed by the call to the close()
method for ReadDevice
. Finally, the catch
block prints “Caught exception;
” to the console. a) Computation on source data is performed in a stream only when the terminal operation is initiated, i.e., streams are “lazy” b) Once a terminal operation is invoked on a stream, it is considered consumed and cannot be used again d) If the stream source is modified when the computation in the stream is being performed, then it may result in unpredictable or erroneous results These three statements are true about streams. Option c) is not correct. Once a stream is created as a sequential its execution mode can be changed to parallel stream by calling parallel()
method. Similarly, once a parallel stream is created, you can make it a sequential stream by calling sequential()
method. a) Integer max = integers.stream().max((i, j) -> i - j).get();
Calling stream()
method on a List<Integer>
object results in a stream of type Stream<Integer>
. The max()
method takes a Comparator
as the argument that is provided by the lambda expression (i, j) -> i - j
. The max()
method returns an Optional<Integer>
and the get()
method returns an Integer
value. Why other options are wrong:
- 选项 b)
Stream
中的max()
方法需要将一个Comparator
作为参数传递 - 选项 c)
List<Integer>
中没有max()
方法 - 选项 d)
mapToInt()
方法返回一个IntStream
,但是max()
方法返回一个OptionalInt
,因此它不能被赋值给Integer
(如上下文中所要求的)
d) This program will result in a compiler error in line marked with NULL
The ifPresent()
method for Optional
takes a Consumer
as the argument and returns void
. Hence, it is not possible to chain the orElse()
method after calling the ifPresent()
method. d) This code will print foo:bar:baz::qux:norf:
The flatMap()
method flattens the streams by taking the elements in the stream. The elements in the given strings are split using the separator “;” and the elements from the resulting string stream are collected. The forEach()
method prints the resulting strings. Why other options are wrong: Option a) This code does not issue any compiler errors Option b) This Splitting an empty string does not result in a null, and hence this code does not throw NullPointerException.
Option c) The syntax of the given regular expression is correct and hence it does not result in PatternSyntaxException.
b) This program prints: 2015-03-01
Since 2015 is not a leap year, there are only 28 days in February. Hence adding a day from 28th February 2015 results in 1st March 2015 and that is printed. d) This code segment throws java.lang.UnsupportedOperationException
The underlying List
object returned by Arrays.asList()
method is a fixed-size list and hence we cannot remove elements from that list. Hence calling removeIf()
method on this list results in throwing an UnsupportedOperationException
. a) Make data members x
and y
private Publicly visible data members violate encapsulation since any code can modify the x
and y
values of a Point
object directly. It is important to make data members private to enforce encapsulation. Why other options are wrong:
- 选项 b)、c)和 d)将
Point
类设为公共,将该类的构造函数设为私有,或者移除 getter 方法,这些都无助于强制封装。 - 选项 e)不能声明静态类。
d) This code prints: sum of 1 to 5 is 15
This code correctly uses Callable<T>
, ExecutorService
, and Future<T>
interfaces and the Executors
class to calculate the sum of numbers from 1 to 5. c) This program prints: 0 The condition within the assert
statement ++num > 0
holds true because num
’s value is 1 with the pre-increment expression ++num
. The expression 0 / 1
results in the value 0 and hence the output. Why other options are wrong:
- 选项 a)和 d)断言条件成立;因此既不会抛出
java.lang.AssertionError
,也不会打印出“失败”消息 - 因为断言是通过传递选项“-ea”来启用的,所以这不会导致被零除。如果断言没有被禁用,它将会因抛出消息“
/ by zero
”而崩溃
b) int val =
Integer.parseInt(integer)
;
Using the method Integer.parseInt(String)
is the correct way to get an int
value from a String
object. The other three options will not compile. d) class AResource implements AutoCloseable {
public void close() throws IOException {
// body of close to release the resource
}
}
AutoCloseable
is the base interface of the Closeable
interface; AutoCloseable
declares close as void close() throws Exception;
In Closeable
, it is declared as public void close() throws IOException;
. For a class to be used with try-with-resources, it should both implement Closeable
or AutoCloseable
and correctly override the close()
method. Option a) declares close()
protected; since the close()
method is declared public in the base interface, you cannot reduce its visibility to protected, so this will result in a compiler error. Option b) declares autoClose();
a correct implementation would define the close()
method. Option c) declares close()
with default access; since the close method is declared public in the base interface, you cannot reduce its visibility to default accesses, so it will result in a compiler error. Option d) is a correct implementation of the AResource
class that overrides the close()
method. a)@FunctionalInterface
interface Foo {
void execute();
}
b) @FunctionalInterface
interface Foo {
void execute();
boolean equals(Object arg0);
}
The interface in option a) declares exactly one abstract method and hence it is a functional interface. In option b) note that equals()
method belongs to Object
class, which is not counted as an abstract method required for a functional interface. Hence, the interface in option b) has only one abstract method and it qualifies as a functional interface. Why other options are wrong:
- 选项 c)接口没有声明抽象方法,因此它不是函数式接口。
- 选项 d)接口没有任何方法,因此它不是一个函数式接口。
c) This program prints: [eeny, meeny, miny, mo]
Stream.of()
method takes a variable length argument list of type T
and it returns a Stream<T>
. The joining()
method in Collectors
class takes delimiter
, prefix
, and suffix
as arguments: joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
Hence, the expression Collectors.joining(", ", "[", "]")
joins the strings with commas and encloses the resulting string within ‘[‘ and ‘]’. f) This code throws SQLException
The try-with-resources block is closed before the while statement executes. Hence, call resultSet.next()
results in making a call on the closed ResultSet
object, thereby throwing an SQLException
. c) return DriverManager.getConnection(url + database, userName, password);
The getConnection()
method in DriverManager
takes three String
arguments and returns a Connection
: Connection getConnection(String url, String user, String password)
Hence, option c) is the correct answer. The other three options will result in compiler errors. b) Stream<String> lines = Files.lines(path);
The lines(Path)
method in Files
class takes a Path
and returns Stream<String>
. Hence option b) is the correct answer. Option a) The code segment results in a compiler error because the return type of lines()
method is Stream<String>
and not List<String>
. Option c) There is no such method named readLines(Path)
in Files
that returns a Stream<String>
and hence it results in a compiler error. Option d) The readAllLines(Path)
method returns a List<String>
and not Stream<String>
and hence the given statement results in a compiler error.