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.