Java基础教程

第一章: 开启Java学习之旅

第二章: 掌握计算机基础知识

第三章: 掌握命令行基础知识

第四章: 我的第一个Java程序

第五章: Java编程必备基础

第六章: Java编程的核心:控制结构

第七章: Java面向对象基础

第八章: Java面向对象进阶

第九章: Java字符串类型

第十章: Java数组与数据结构

第十一章: Java高级数据结构

第十二章: Java并发编程基础

首页 > Java基础教程 > 第八章: Java面向对象进阶 > 8.3节: 多态与抽象基类

8.3节: 多态与抽象基类

薯条老师 2021-12-03 12:10:12 185168 0

编辑 收藏

广州番禺Python, Java小班周末班培训

第四期线下Java, Python小班周末班已经开课了,培训的课程有Python爬虫,Python后端开发,Python办公自动化,Python大数据分析, Java中高级工程师培训。授课详情请点击:http://chipscoco.com/?cate=6    

8.3.1 理解多态

在面向对象的程序设计中,多态一般是指父类方法的多种不同实现。如何对父类方法进行多种不同的实现?可以通过继承的方式,即多个子类对从父类继承的方法进行不同的实现。

子类对父类方法进行不同的实现以后,将子类再赋值给父类对象,父类对象再根据子类对象的类型,在运行时调用同一方法的不同实现,这样就实现了多态。

举个简单的例子来帮助同学们理解,在蒙面歌手大赛中,台上站了3位蒙面选手,观众需根据每位选手的演唱特点来猜出歌手的姓名。这里的歌手就相当于父类,唱歌是父类提供的方法。每一个蒙面的歌手是子类,有自己独特的演唱风格。由于蒙了面,所以观众并不知道是哪位歌手,需要等到歌手唱歌的时候,根据歌手的演唱特点,才能大概猜出是哪位歌手。

观众只知道站在台上的是歌手(父类),但歌手唱歌时,是根据自己(子类)的演唱风格来进行演唱(运行时)。

8.3.2 父类与子类

多态是通过将子类对象赋值给父类对象来实现的,现在将8.3.1节中的蒙面歌手例子用Java语言来实现:

class MaskedSinger{
    public void sing(String song){
        System.out.println(song);
    }
}

 
class LiBai extends MaskedSinger {
    public void sing(String song){
        System.out.println("喝酒");
        System.out.println(song);
    }
}


class BaiJuYi extends MaskedSinger {
    public void sing(String song){
        System.out.println(song);
        System.out.println("喝酒");
    }
}

 
public class HelloJava {
    public static void main(String[] args) {
        // 将子类对象赋值给父类对象
        MaskedSinger singer = new LiBai();
        // 父类在运行时根据子类的类型来调用子类的方法
        singer.sing("将进酒");
        // 将子类对象再次赋值给父类对象
        singer = new BaiJuYi();
        // 父类在运行时根据子类的类型来调用子类的方法
        singer.sing("琵琶行");
    }
}

读者需注意,在以上代码中是将子类对象赋值给父类。singer对象的类型在编译时已确定,是MaskedSinger类型,但是在执行期调用的却是子类LiBai和子类BaiJuYi的方法。在将子类对象向上赋值给父类引用时,Java会将子类对象切割,使得父类对象仅能调用从父类中定义的方法。下图展示了子类对象的切割过程:

图片.png 

现在写一段代码来验证子类对象是否被切割:

class MaskedSinger{
    public void sing(String song){
        System.out.println(song);
    }
}

 
class LiBai extends MaskedSinger {
    public void sing(String song){
        // 先喝酒再吟唱
        System.out.println("喝酒");
        System.out.println(song);
    }
    // 子类Libai新增的write方法
    public void write(String poem){
        System.out.println(poem);
    }
}

 
public class HelloJava {
    public static void main(String[] args) {
        // 将子类对象赋值给父类对象
        MaskedSinger singer = new LiBai();
        // 父类在运行时根据子类的类型来调用子类的方法
        singer.sing("将进酒");
        // singer调用write方法失败,因为singer是一个MaskedSinger类型,该类型没有提供write方法
        singer.write("蜀道难");
    }
}

读者在IDEA中执行以上代码时,Java会抛出错误信息,因为singer是MaskedSinger类型,并未提供write方法,write方法是在子类Libai中定义的,Java在编译时会去查找父类MaskedSinger是否有提供该方法,若没有,则会编译失败。

8.3.3 运行时多态

与运行时相对的是编译时,所谓的编译时,是指Java的编译器将Java源代码翻译为Java中间字节码的过程。

Java的中间字节码是供Java虚拟机执行的一种虚拟指令格式, 关于Java虚拟机,会在后面的教程中详细讲解。

在编译期间,Java会检查源程序的语法,并对源代码做一定程度的优化。而运行时,是指Java的解释器将Java中间字节码再翻译成机器指令并执行的过程。

CPU只能执行机器指令,所以为什么需要将Java中间字节码再翻译为CPU可执行的指令。

多态是指同一个方法,却表现出不同行为。读者可这么来简单理解,明明是父类对象,但在运行时调用的却是子类对象重写后的方法,这样就很契合"多态"。如果将父类对象赋值给父类,或将子类对象赋值给子类,那么就没有这样的多态效果,因为方法的调用在编译期就已经确定下来了。

8.3.4 抽象基类

面向对象程序设计有一个很重要的里氏替换原则,关于里氏替换原则,在7.2.3节已经做了简单介绍。里氏替换原则是说任何使用基类的地方 ,也可以用其派生类来替换,不会影响程序的执行结果(即,不论使用基类还是派生类,得到的都是期望的结果)。

理解这个设计原则,读者要思考的是,在哪些情况下使用子类来替换会影响程序的执行结果。

本节的主旨是讲解Java中的多态,要实现多态,子类必须重写父类中的方法,那么问题来了,方法的多态会不会影响程序的执行结果?

违背里氏替换原则,会破坏类的继承体系,带来一些潜在的问题,影响程序的可维护性。

这需要具体问题具体分析,如果子类重写父类方法时,改变了父类方法的语义,那么肯定无法得到一致的结果,比如父类的avg方法用來计算算数平均值,但是子类却将该方法重写为计算几何平均值, 那么在用子类来替换时肯定得不到预期的结果。

子类不应该修改父类的状态和方法语义,如果一定要修改,請重新定义一个不同的方法。

如果子类重写父类方法只是修改原有的实现逻辑,比如采用一个更优的算法,那么仍然符合里氏替换原则。举个简单的例子,经典的排序算法有冒泡排序,快速排序,堆排序等,假设父类的sort方法采取的是普通的冒泡排序,而子类将sort方法重写为堆排序,这样不论用父类还是用子类得到的都是一致的结果。通常,更优的做法是让子类继承于抽象基类或接口,基类不再提供方法的具体实现。这样设计的优点是将抽象层和实现层隔离,父类只需提供基本的抽象,而将具体的实现和扩展交给子类。在Java中定义抽象基类需使用abstract关键字,至于接口,会在8.4节中详细讲解。

读者须知:抽象基类不能被实例化,只能被继承。

定义抽象基类的语法结构:

[modifier] abstract class AbstractClassName {
            ;
}

定义抽象基类时,也可以使用abstract来定义抽象方法,抽象方法只包含一个方法名,而没有方法体。现将8.3.2节中的MaskedSinger改成抽象基类,代码实例如下:

/*
抽象基类应该只提供基本的抽象,而将具体实现和扩展逻辑交给子类。
 */
 
abstract class MaskedSinger{
// 抽象方法只包含方法名,不会提供具体实现。
    public abstract void sing(String song);
}
 
 
class LiBai extends MaskedSinger {
    public void sing(String song){
        System.out.println("喝酒");
        System.out.println(song);
    }
}


class BaiJuYi extends MaskedSinger {
    public void sing(String song){
        System.out.println(song);
        System.out.println("喝酒");
    }
}
 
public class HelloJava {
    public static void main(String[] args) {
        // 将子类对象赋值给抽象基类
        MaskedSinger singer = new LiBai();
        // 在运行时根据子类的类型来调用子类的方法
        singer.sing("将进酒");
        // 将子类对象再次赋值给抽象基类
        singer = new BaiJuYi();
        // 在运行时根据子类的类型来调用子类的方法
        singer.sing("琵琶行");
    }

8.3.5 课后习题

(1) 简述你对多态的理解,并举一个生活中的例子来说明。

(2) Java执行程序前会有一个编译过程,请回答Java编译器在这一期间主要做了哪些工作。

(3) 如何理解运行时多态?请读者思考,既然有运行时多态,那么还有编译时多态吗?如果有,请写出一个代码实例来加以说明。

(4) 本节提到了抽象层和实现层的隔离,请读者思考为什么要将二者隔离,这样做的优点是什么?

(5) 抽象基类不能实例化,其主要的作用是为子类提供基本的抽象。请读者定义一个表示编程语言的抽象基类,该基类需为Python等子类提供基本抽象。

8.3.6 高薪就业班



欢迎 发表评论: