请不必担心 Oracle职业认证(OCP)Java SE 7 程序员认证 会如何用Java方法覆盖为难你。
本文摘自《 OCP Java SE 7 程序员II认证指南 》,内容涉及Java方法覆盖和虚拟调用,包括考试中可能遇到的陷阱和技巧。
你庆祝节日或好事的方式和父母一样吗?还是稍有不同?也许庆祝同样的节日或事件,会用自己独特的方式。类似的,类能够继承其他类的行为。但是它们也能够重新定义继承的行为——也称 方法覆盖 。
方法覆盖是面向对象编程语言的特征,它使派生类能够定义从基类集成的方法实现,以扩展自己的行为。派生类能够通过定义具有相同方法原型或方法名称、数量和参数类型的实例方法,覆盖实例基类中定义的方法。被覆盖的方法也与多态方法作用相同。基类的 静态方法 不能覆盖,但能够用相同的原型定义隐藏在派生类。
能被派生类覆盖的方法叫做虚拟方法。 但是注意: Java 已经弃用此词,在 Java 词汇中没有“虚拟方法”一说。该词用在其它面向对象语言中,如 C 和C++。虚拟方法调用是指调用基于对象引用的类型正确地被覆盖方法,而不是调用对象引用本身。虚拟方法在运行时确定,而非在编译时。
OCPJava SE 7 程序员认证II考试会考查方法覆盖;方法覆盖的正确语法;重载、覆盖和隐藏方法之间的区别;运用覆盖方法时的一般错误;以及虚拟方法调用。让我们从方法覆盖开始。
注:基类方法指被 覆盖的方法 、派生类方法指 覆盖方法 。
我们继承父母的行为,但会重新定义某些继承行为以便适应我们自身需要。同样地,派生类能继承基类的行为和属性,但仍然有所差异——用自己的方式定义新的变量和方法。派生类也能通过覆盖来为基类定义不同的行为。这里举一个例子,Book类定义一个方法issueBook(),把天数作为方法的参数。
class Book { void issueBook(int days) { if (days > 0) System.out.println("Book issued"); else System.out.println("Cannot issue for 0 or less days"); } }
接下来是另一个类,CourseBook,它继承了Book类。该类需要覆盖issueBook(),因为如果只是为了引用,那么就CourseBook不能发行。同样,CourseBook不能发行超过14天。我们来看看是怎样用覆盖issueBook()方法来完成。
class CourseBook extends Book { boolean onlyForReference; CourseBook(boolean val) { onlyForReference = val; } @Override #1 void issueBook(int days) { #2 if (onlyForReference) System.out.println("Reference book"); else if (days < 14) super.issueBook(days); #3 else System.out.println("days >= 14"); } } #1 注解:@Override #2 覆盖基类Book中的OverridesissueBook() #3 调用Book中的issueBook()
(#1)处的代码用了注解 @Override,告知编译器该方法覆盖了基类的一个方法。尽管这个注释是非强制的,但如果你错误地覆盖一个方法,该注释会非常有用。(#2)定义issueBook()方法与类Book中相同的名字和方法参数。(#3)调用类Book中定义的issueBook()方法,然而,它不是强制的。要看派生类是否要执行基类中同样的代码。
注:每当你打算覆盖派生类中的方法时,请使用注释@Override。如果一个方法不能被覆盖或实际上在重载而不是覆盖一个方法,它就会给你警告。
下面的例子能够用于测试先前的代码:
class BookExample { public static void main(String[] args) { Book b = new CourseBook(true); b.issueBook(100); #A b = new CourseBook(false); b.issueBook(100); #B b = new Book(); #C b.issueBook(100); #D } } #A 输出 “Reference book” #B 输出 “days >= 14” #C b此时指向Book的一个实例 #D 输出 “Book issued”
图1 展现了类BookExample的编译和执行过程,第一步和第二步如下:
图1 为了编译b.issueBook(),编译器只指向类Book的定义。为了执行b.issueBook(),Java运行时环境( JRE )使用类CourseBook中的issueBook()实际 实现的方法 。
现在让我们探讨怎样在派生类中正确地覆盖基类方法。
我们以覆盖review方法为例,如下所示:
class Book { synchronized protected List review(int id, List names) throws Exception { #A return null; } } class CourseBook extends Book { #B @Override final public ArrayList review(int id, List names) throws IOException { #C return null; } } #A 基类Book中的review方法 #B CourseBook继承了Book #C 派生类CourseBook中被覆盖的方法review
图2显示了方法声明的构成:访问修饰符、非访问修饰符、返回类型、方法名称、参数列表,以及能抛出的异常的列表(方法声明与方法签名不同)。该图就基类Book中定义的review方法和类CourseBook覆盖方法review()各自标识的部分也进行了比较。
图2 比较方法声明基类方法和覆盖方法的组件
表1比较图2中显示的方法组件
表1:方法组件和覆盖方法可接受值的比较
方法组件 | Book 类中的值 | CourseBook 类中的值 | 覆盖CourseBook类里的review()方法 |
访问修饰符 | protected | public | 定义与基类review() 方法相同访问级别或限制更小的访问级别 |
非访问修饰符 | synchronized | final | 方法覆盖能使用被覆盖方法的任意非访问修饰符。非抽象方法也能覆盖成抽象方法。但基类中的 final 方法不能被覆盖。静态方法不能覆盖后改为非静态方法。 |
返回类型 | List | ArrayList | 定义基类中方法相同或子类型的返回类型(协变量 covariant 返回类型) |
参数名字 | review | review | 精确匹配 |
参数列表 | (int id, List names) | (int id, List names) | 精确匹配 |
抛出异常 | throws Exception | throws IOException | 基类方法抛出none、相同异常或子类异常 |
考点提示:表1所列关于覆盖方法异常的规则只应用于检查异常。覆盖方法能抛出未检查的异常(运行时异常或错误),即使覆盖方法没有抛出。未检查的异常不是方法原型的部分,编译器不负责检查。
第6章包括对覆盖和覆盖方法排除异常详细的解释。我们来过一下几个重要并且很有可能出现在考题中的无效代码组合。
注:尽管这是最好的练习,我故意没有在方法覆盖的定义前面加上注解@Override,因为你可能不会在考试中碰到。
派生类能分配同样的或更多的访问权限,但不能分配派生类中覆盖方法更小的访问权限:
class Book { protected void review(int id, List names) {} } class CourseBook extends Book { void review(int id, List names) {} #A } #A不能编译;派生类覆盖方法不能用更小的访问权限
派生类不能覆盖标记为final的基类方法。
class Book { final void review(int id, List names) {} } class CourseBook extends Book { void review(int id, List names) {} #A } #A 不能编译;标记了final的方法不能覆盖
覆盖方法范围子类被覆盖方法返回类型,叫协变量返回类型。覆盖方法、基类和派生类中方法的参数列表必须完全一样。如果试着在参量列表中用协变量类型,你将会重载方法而非覆盖它们。例如:
class Book { void review(int id, List names) throws Exception { #1 System.out.println("Base:review"); } } class CourseBook extends Book { void review(int id, ArrayList names) throws IOException { #A System.out.println("Derived:review"); } } #1 参数list—int和List #A 参数list—int和ArrayList
(#1)基类 Book 中review()收到一个类型列表List对象。派生类CourseBook中review()方法收到一个子类型列表(ArrayList事项实现了 List)。这些方法没有被覆盖——它们被重载了:
class Verify { public static void main(String[] args)throws Exception { Book book = new CourseBook(); #1 book.review(1, null); #A } } #1 引用变量指向CourseBook目标 #A 调用Book的review方法;输出“Base:review”
(#1)处的代码 Book 的引用变量指向CourseBook对象。编译过程从基类 Book 分配review()方法执行到引用变量book。因为类CourseBook中review方法没有覆盖类Book中review方法,至于是调用类Book中review()方法还是类CourseBook中review()方法,JRE 不会犯丁点迷糊。它会直接调用类Book中review()方法。
考点提示:引用变量类型,重载方法选择。这个选择在编译时间做出。
重载方法必须声明不抛出异常、相同异常或基类声明的子类型异常或编译失败。然而,该规则不应用于错误类或运行时异常。例如:
class Book { void review() throws Exception {} void read() throws Exception {} void close() throws Exception {} void write() throws NullPointerException {} void skip() throws IOException {} void modify() {} } class CourseBook extends Book { void review() {} #A void read() throws IOException {} #B void close() throws Error {} #C void write() throws RuntimeException {} #D void skip() throws Exception {} #E void modify() throws IOException {} #F } #A 编译通过;声明不抛出异常。 #B 编译通过;声明抛出IO异常(一个子类异常)。 #C 编译通过;覆盖方法能声明抛出任何错误。 #D 编译通过;覆盖方法能声明抛出任何运行时异常。 #E 编译失败;声明抛出异常(IO异常的超类)。覆盖方法不能声明抛出比被覆盖方法更多的异常。 #F 编译失败;声明抛出IO异常。覆盖方法不能声明抛出检查异常,如果被覆盖方法没有声明。
考点提示:覆盖方法能声明抛出运行时异常或错误,即使被覆盖类没有声明。
为了记住先前的知识点,我们来用怪物与异常作类比。图3展现了有趣的记忆方法,当被覆盖方法不声明抛出checked异常和声明抛出时,覆盖方法能够列出的异常(怪物)。
图3 异常与怪物对比。当覆盖方法声明抛出 受检 异常(怪物),覆盖方法能声明抛出none、同样的异常或 级别较低的受检 异常。覆盖方法能声明抛出任何错误或运行时异常。
简单的回答是不行。你只能覆盖基类的以下方法:
派生类中方法的可访问性依赖访问修饰符。例如,基类定义的私有方法不能被派生类使用。同样,基类默认访问方法不能被另一个包中的派生类使用。一个类不能覆盖不能访问的方法。
如果派生类定义一个与基类中相同名字和原型的静态方法,它将基类方法隐藏起来,并且不覆盖它。你不能覆盖静态方法。例如:
class Book { static void printName() { #A System.out.println("Book"); #A } #A } class CourseBook extends Book { static void printName() { #B System.out.println("CourseBook"); #B } #B } #A 基类中的静态方法 #B 派生类中的静态方法
类CourseBook的printName()方法隐藏了类Book 的printName()方法中。没有覆盖它。因为静态方法固定在编译时,调用哪个printName()方法要看引用变量的类型。
class BookExampleStaticMethod { public static void main(String[] args) { Book base = new Book(); base.printName(); #A Book derived = new CourseBook(); derived.printName(); #B } } #A 输出“Book” #B 输出“Book”
方法覆盖、重载和隐藏容易混淆。 图4 对这些方法在Book和CourseBook类进行了区分。左侧是类定义,右侧是UML图。
图4 中辨析基类和派生类中的方法覆盖、方法重载和方法隐藏。
考点提示:当一个类集成另一个类时,它能够重载、覆盖或隐藏基类的方法。类不能覆盖或隐藏自己的方法——只能重载自己的方法。
派生类覆盖或隐藏基类中的静态或非静态方法,下面我们用“Twist in the Tale”练习来检查派生类中定义静态或非静态方法的正确代码(练习答案列在文末)。
每章(本文摘录出处)包含若干“Twist in the Tale”练习。为了这些练习,我试着修改已经包含在章节中的例子,标题“Twist in the Tale”的意思是指经过修改或改进的代码。这些练习强调了如何通过细微的调整改变代码的行为,一定会激励你在考试中认真检查所有代码。
我收录这些练习的主要理由是,在真正的考试中你可能会被要求回答几个看起来一模一样问题。但仔细检查,你会发现这些问题之间差别细微,正是这些差异改变了代码行为和正确答案项。
我们来修改类 Book 和CourseBook的代码,并在两个类中定义多组静态和非静态方法print()如下:
(a)
class Book{ static void print(){} } class CourseBook extends Book{ static void print(){} }
(b)
class Book{ static void print(){} } class CourseBook extends Book{ void print(){} }
(c)
class Book{ void print(){} } class CourseBook extends Book{ static void print(){} }
(d)
class Book{ void print(){} } class CourseBook extends Book{ void print(){} }
你的任务是选取其中一个,然后在系统中进行编译,看看是否正确。在实际考试中,你需要验证(没有编译器)代码片段是否能够编译通过:
简单的回答是“不”。构造器不能被派生类继承。因为只有被继承的方法能被覆盖,所以构造器不能被派生类覆盖。如果考题要你覆盖基类构造器,这是在给你下套,你懂的。
考点提示:构造器不能被覆盖,因为基类构造器不能被派生类继承。
目的: 区分重载、覆盖和隐藏方法。
答案解析: (a)编译成功。类CourseBook的静态方法print()隐藏了基类Book中静态方法print()。
class Book { static void print() {} } class CourseBook extends Book { static void print() {} }
实例方法能够覆盖基类的方法,但静态方法不能这么做。当派生类定义一个与基类具有相同原型的静态方法,它会将其隐藏。静态方法不具有多态性。
(b)不能编译。基类Book的静态方法print()不能被派生类CourseBook中的实例方法print()隐藏。
class Book { static void print() {} } class CourseBook extends Book { void print() {} }
(c)无法编译。基类 Book 中的实例方法print()不能被派生类CourseBook静态方法print()覆盖。
class Book{ void print() {} } class CourseBook extends Book { static void print() {} }
(d)编译成功。类CourseBook中的实例方法print()覆盖了基类 Book 中的实例方法print():
class Book{ void print() {} } class CourseBook extends Book { void print() {} }
祝你好运,成功取得认证!
致礼,
马拉
原文链接: dzone 翻译:ImportNew.com -sinofalcon
译文链接:[]