Java新手指南:方法覆盖

请不必担心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”练习。为了这些练习,我试着修改已经包含在章节中的例子,标题“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(){}
}

你的任务是选取其中一个,然后在系统中进行编译,看看是否正确。在实际考试中,你需要验证(没有编译器)代码片段是否能够编译通过:

  • 覆盖 print 方法
  • 隐藏 print方法
  • 编译错误

可以覆盖或虚拟调用基类构造器吗?

简单的回答是“不”。构造器不能被派生类继承。因为只有被继承的方法能被覆盖,所以构造器不能被派生类覆盖。如果考题要你覆盖基类构造器,这是在给你下套,你懂的。

考点提示:构造器不能被覆盖,因为基类构造器不能被派生类继承。

“Twist in the Tale”答案

目的:区分重载、覆盖和隐藏方法。

答案解析:(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
译文链接: http://www.importnew.com/18287.html
[ 转载请保留原文出处、译者和译文链接。]



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部