专栏名称: ImportNew
伯乐在线旗下账号,专注Java技术分享,包括Java基础技术、进阶技能、架构设计和Java技术领域动态等。
目录
相关文章推荐
Java编程精选  ·  支付宝P0级重大事故:整整5分钟所有订单打8 ... ·  4 天前  
芋道源码  ·  SpringBoot ... ·  2 天前  
芋道源码  ·  Redis实现分页+多条件模糊查询组合方案! ·  3 天前  
芋道源码  ·  4 种 MySQL 同步 ES 方案,yyds! ·  3 天前  
芋道源码  ·  Guava的这些骚操作,让我的代码量减少了50% ·  4 天前  
51好读  ›  专栏  ›  ImportNew

Java 方法覆盖详解

ImportNew  · 公众号  · Java  · 2017-06-03 12:38

正文

(点击上方公众号,可快速关注)


编译:ImportNew - sinofalcon ,

如有好文章投稿,请点击 → 这里了解详情


请不必担心 Oracle职业认证(OCP)Java SE 7 程序员认证 会如何用Java方法覆盖为难你。


http://education.oracle.com/pls/web_prod-plq-dad/db_pages.getpage?page_id=5001&get_params=p_exam_id:1Z0-804


本文摘自《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:方法组件和覆盖方法可接受值的比较


考点提示:表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() {}

}


祝你好运,成功取得认证!


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能