写点什么

Java | abstract 关键字【面向对象的第三大特征——多态】

作者:Fire_Shield
  • 2022 年 9 月 28 日
    浙江
  • 本文字数:10061 字

    阅读完需:约 33 分钟

Java | abstract关键字【面向对象的第三大特征——多态】

Hello 大家好,讲完了类与对象的两大基本特征,本文就让我们一起进入面向对象的第三大特征——多态,感受多态所带来的魅力:rose:


@TOC

一、上转型对象的引入

1、什么是向上转型

所谓向上,用一句话来讲就是【父类引用指向子类对象】,对于上转型对象,它可以说是多态的雏形,要学好多态必须先了解上转型对象。好,废话不所说,直接上代码


public class Animal {    String name;
public Animal(String name) { this.name = name; } void eat(){ System.out.println(this.name + "正在吃东西"); }}
复制代码


public class Cat extends Animal{    public Cat(String name) {        super(name);    }
@Override void eat() { System.out.println(this.name + "正在吃鱼"); }}
复制代码


public class test {    public static void main(String[] args) {        Animal animal = new Cat("咪咪");        animal.eat();    }}
复制代码



  • 从上述代码和运行示例可以看出,对于上转型对象就是父类引用变量调用的方法是子类覆盖或继承父类的方法,而不是父类的方法。但是调用的属性还是父类的属性

2、发生向上转型的三种时机

接下去我们来了解一下产生上转型对象的三种方式,可以可以帮我们在实例开发中快速的判断自己是否在使用上转型对象


  • 第一种,直接赋值,将子类对象给到父类引用


这种的话就是我上面作为引入的案例


Animal animal = new Cat("咪咪");animal.eat();
复制代码


  • 第二种,使用方法传参的形式


从下面的代码可以看出,是将一个本类方法的形参设置为父类的引用,然后将子类的对象作为实参传入,这也可以形成上转型对象


public static void func(Animal animal){    animal.eat();}public static void main(String[] args) {    Cat cat = new Cat("咪咪");    func(cat);}
复制代码


  • 第三种,使用返回值形成上转型对象


这种方式比较抽象而且难理解,因为是采用父类这个类型作为返回值,因为直接 new 一个子类的对象去返回,就自然地形成了上转型对象,这个确实不太好理解,大家要自己观察一下,就不给代码,给的指示图片


3、使用上转型对象的注意要点

说完了三种上转型对象的方式,接下去我们说说使用在这个上转型对象时我们需要注意哪些


  • 上转型对象不可以访问子类新增的成员变量和子类自己新增的方法,因为这是子类独有的,而父类中没有

  • 上转型对象可以访问子类继承的方法或者是子类重写的方法,这个时候==当上转型对象去调用这个方法时,一定是调用了子类重写后的方法==,这就是我们前面在讲继承的时候所提到的方法重写

  • 不可以将父类创建的对象的引用赋值给子类声明的对象,也就是下面的这两句代码,这很明显和我们的上转型对象相反的,我们是将子类对象给到父类的引用,但这是将父类的引用给到子类的对象,完全就是颠倒黑白【就和猫是动物,动物却不是猫🐱一个道理】


Animal animal;Cat c = animal;
复制代码

4、向上转型与接口回调的区别

好,最后我们再来说一下上转型对象和接口回调的区别,接口的话会在下一篇文章讲【implements 关键字】的时候讲到,这里先提一嘴


  • 对于向上转型的话,它是牵涉到多态和运行期绑定的范畴,这里的运行绑定我没有讲,因为太偏了,不符合本文的主旨,感兴趣的小伙伴可以去了解一下Java中的动态绑定

  • 而对于这个接口回调的话,它和向上转型非常得类似,是将一个实现某一接口的==类创建的对象赋值给该接口声明的接口变量==,使得该接口变量可以去调用被类实现的接口方法接口提供的 Default 方法

  • 好,就不多说,请看我的下一篇文章😀

二、继承与多态的联系

1、多态的基本概念及引入

讲到了多态,也不能忘了继承,接下去就让我们来讲讲继承和多态之间的联系,以及它们都有哪些优点


  • 首先来讲一讲多态的概念,对于多态,就是不同对象对同一物体或事件==发出不同的反应或响应==,比如 student 是一个父类,那么在操场上上体育课的学生和在教室里面的学生就是它的子类。这时上课铃声响了,【上体育课的学生】去操场,【在教室里面上课的学生】则是回教室,不同的学生有着不同的反应,这就是多态


然后来看一个小案例


class Animal{    void cry(){    }}
复制代码


class Dog extens Animal{    void cry(){        System.out.println("汪汪~~");    }}
复制代码


class Cat extens Animal{    void cry(){        System.out.println("喵喵~~");    }}
复制代码


class test{    public static void main(String[] args) {        Animal animal;        animal = new Dog();        animal.cry();
animal = new Cat(); animal.cry(); }}
复制代码


  • 从上述的小案例可以看出,对于继承,它可以实现代码的复用性,将父类中的方法给到继承的子类继续去使用,不需要再重写定义;而对于多态,它能够让类的调用这连这个类的类型都可以不必知道是什么,只需要这个对象具有哪个具体的方法即可,因为只需要去调用具体的方法,就可以通过父类的引用去调用子类对象然后访问子类重写的这个方法

2、要产生多态的条件

了解了多态的基本概念之后,我们要接下来说说要如何去实现多态


  • 要发生向上转型【父类引用 引用子类对象】

  • 子类和父类都要有同名的覆盖方法

  • 通过父类引用调用重写方法时形成运行时绑定

3、多态的好处

清楚了如何产生多态的条件,那我们就要去想,这个多态究竟有什么好处呢,为什么她是面向对象中最终要的一趴,接下来我们来说说这个


  • ①类调用者对类的使用成本进一步降低

  • ②能够降低代码的【圈复杂度】,避免使用大量的 if-else

  • 由于这一点好处可能不太好理解,我们讲个具体的案例分析一下


class Geometry{    void draw(){        System.out.println("画一个几何图形");    }}class Circle extends Geometry{    @Override    void draw() {        System.out.println("画一个⚪");    }}class Slice extends Geometry{    @Override    void draw() {        System.out.println("画一个♦");    }}class Triangle extends Geometry{    @Override    void draw() {        System.out.println("画一个▲");    }}
复制代码


public class test2 {    public static void main(String[] args) {        Circle circle = new Circle();        Slice slice = new Slice();        Triangle triangle = new Triangle();        String[] shapes  = {"cycle","triangle","slice","cycle"};        for(String shape : shapes)        {            if(shape.equals("cycle")){                circle.draw();            }else if(shape.equals("triangle")){                triangle.draw();            }else if(shape.equals("slice")){                slice.draw();            }        }    }}
复制代码


  • 从上述案例可以看出,我们是创建了一个几何图形类,然后用三个不同的几何图形类去继承这个父类,然后重写父类中的 draw()方法,在主方法接口中我们可以看到,是通过一个遍历和判断的形式去访问这个 Shapes 对象数组中的值,通过 equals()这个 API 去判断字符串是否符合,符合的话再用不同的对象去调用各自的方法

  • 从中我们可以看到,这个写法是比较冗杂又常规的,那有没有更好的方法呢?

  • 这个时候就可以使用我们多态的思想了,因为父类和子类中有同样且重名的方法,也进行了重写,我们可以利用将子类对象给到父类引用这个方法,去形成一个上转型对象,==通过多态的形式来简化代码==


OK,是时候展现真正的技术了:sunglasses:


Geometry[] shapes = {new Circle(),new Triangle(),new Slice(),new Circle()};for(Geometry shape : shapes){    shape.draw();}
复制代码


  • 对,就是这么简便,我们可以直接在这个对象数组中放 new 出来的子类对象,然后在遍历这个数组的时候便发生了上转型对象,利用父类的引用 shape 去调用每一次传入进来的子类对象,就可以去独立地方法那个子类所重写的方法,这就很好地==体现了多态的思想==

  • ③对于最后这一点,还是比较好理解的,就是可扩展能力强【无限地增加继承的子类】

  • 比如说你要加一个正方形类,只需要把这个类实现一下然后去继承一下 Geometry 父类即可,完全不需要改动任何的代码,这就是多态的优势之处


看完了上面这些,那您就算初步地了解了多态这个概念,但是并没有形成那个思维,只是一个引入,接下来我们便通过 abstract 这个关键字真正地进入多态的编程模式,感受面向抽象的编程思维:walking:

三、abstract 关键字【抽象类与抽象方法】

1、保姆级细致引入!!!

  • 首先我们来分析一下上面 Geometry 父类,大家有没有发现父类中的这个 draw()方法写了和没写一样,因为完全就没有产生这么一个调用


class Geometry{    void draw(){        System.out.println("画一个几何图形");    }}
复制代码


  • 为此我们就可以将此方法设置为抽象方法,在返回值类型前加上一个 abstract 关键字即可,然后抹去这个方法的方法体,因为==抽象方法是不可以有方法体的==


abstract void draw();
复制代码


  • 但是这样写的话编译器马上就给我们报错了,说是一定要将这个类也抽象,这就形成了抽象类的概念,因为如果一个类中有抽象方法,那么这个类就必须是抽象类

2、注意事项

那这时候就有同学说,哇,这个关键字很厉害、很高级的感觉。是的,不然我不会前面铺垫那么多,才讲到这个关键字,厉害归厉害,但是在使用这个关键字的时候要注意的地方还是挺多的,让我们一起来看一下


  • ①抽象类不能被实例化


为什么不能被实例化呢?都说了它是抽象的嘛,怎么能会有一个具体的东西呈现给你呢,是吧所以下面这步操作你不可以做:point_down:


Geometry shape = new Geometry();
复制代码


  • ②类内的数据成员,和普通类没有区别


此话怎讲呢?也就是这个抽象类,它除了不能对抽象方法做实现外,其他其实也和普通的类没什么区别,普通的成员变量和成员方法都是可以定义的


abstract class Geometry{    private int num;    public void func(){        System.out.println("这是抽象类的一个实例方法");    }    abstract void draw();}
复制代码


  • ③抽象类主要就是用来被继承的,所以不可以被 final 关键字修饰,抽象方法也是一样,需要被重写


这个的话就要理解,因为抽象类的话已经不可以被实例化对象了,那你再不能继承它然后做一些操作的话,那这个类不就没用了吗,是吧😀,然后这个抽象方法的话,你在抽象类中没有重写它,在继承子类中也没有对其进行一个重写,也是很荒谬的一件事,上一篇文章中我们有详细讲过final关键字,说到了 final 这个关键字如果去修饰方法的时候,那么这个方法就不可以被重写,如果去修饰类的话,那么这个类就不可以被继承,所以大家一定不要把==abstract 和 final 关键字==写在一起,这是矛盾的


  • ④如果一个类继承了这个抽象类,那么这个类必须重写抽象类中的抽象方法


这个的话在上面也讲到过了,如果一个类去继承了一个抽象类,那么你就必须重写其中的抽象方法,否则的话要么你自己也定成一个抽象类,然后继承你的子类就必须要重写你父类的方法,就如下面的代码一样


abstract class ff extends Geometry{    }
class gg extends ff{ @Override void draw() { }}
复制代码


  • ⑤不可以用 staic 和 private 关键字修饰 abstract 方法


因为static关键字指的是静态方法,是可以被所有同包下的类所调用的,然后其他类就可以重写这个方法自己用,这就违背了继承的特性,只有子类才可以重写父类的方法然后的话 priavate 关键字因为是私有的,那子类就是无法访问的,私有的成员变量还可以通过 get()方法来访问一下,如果父类的方法都私有化了,那么子类是无法去访问的

3、具体案例

讲了这么多有关 abstract 关键字的注意事项,现在就让我们到实战中来看看它是具体怎么应用的吧:point_left:


女朋友类(doge)


public abstract class GirlFriend {    abstract void speak();    abstract void cooking();}
复制代码


中国女朋友类(doge)


public class ChinaGirlFriend extends GirlFriend{    @Override    void speak() {        System.out.println("你好");    }
@Override void cooking() { System.out.println("会做水煮鱼"); }}
复制代码


美国女朋友类(doge)


public class AmericanGiralFriend extends GirlFriend{    @Override    void speak() {        System.out.println("Hello");    }
@Override void cooking() { System.out.println("Can make roast beef"); }}
复制代码


男孩类


public class Boy {    GirlFriend girlFriend;    void setGirlFriend(GirlFriend f){        girlFriend = f;    }    void showGirlFriend(){        girlFriend.speak();        girlFriend.cooking();    }}
复制代码


测试类


public class test {    public static void main(String[] args) {        GirlFriend girlFriend = new ChinaGirlFriend();        Boy boy = new Boy();        System.out.println("中国女朋友");        boy.setGirlFriend(girlFriend);        boy.showGirlFriend();
girlFriend = new AmericanGiralFriend(); System.out.println("--------------"); System.out.println("美国女朋友"); boy.setGirlFriend(girlFriend); boy.showGirlFriend(); }}
复制代码


运行结果



  • 好,在看完这段代码后我们来讲解一下,可以看到,首先是创建了一个抽象的女朋友类,然后定义了两个抽象方法,一个是说话,一个是做饭

  • 然后一个中国女朋友类和美国女朋友类分别去继承这个父类,重写里面的方法,然后通过一个男孩类作为一个过渡(doge),设置一个方法去接收两个女孩,形成一个上转型对象,以此来实现多态

四、面向抽象的编程思维

在看完 abstract 关键字的应用之后,您对多态有没有形成一个概念了呢,其实要实现多态还是要有一个面向抽象的编程思维,这一点是很重要的:key:


  • 我们在企业开发中经常是使用 abstract 类,其原因是 abstract 类只关心操作,不关心这些操作的具体实现,可以使程序的设计者把主要精力放在程序的设计上,而不必拘泥于细节的实现(将这些细节交给子类的设计者),这样就可以使得整体项目框架设计者不必把大量的时间和精力花费在具体的算法上

  • 举个很简单的现实生活中的例子,一家出版世界地图的厂家,当设计师在设计一块国家区域的时候,不必去考虑诸如城市中的街道牌号等细节,细节应当由抽象类的非抽象子类去实现,这些子类可给出具体的实例,从而去完成程序功能的具体实现。

  • 所以当我们在设计一个小程序时,可以通过在 abstract 类中声明若干个 abstract 方法表明这些方法在整体系统设计中的重要性,方法体的内容细节由它的非 abstract 子类去完成


———— 以上选段摘自耿祥义《Java2 实用教程》

五、多态的经典案例剖析

了解了面向抽象的编程思维,以及看了这么多的有关多态的小案例,接下来就让我们到实战中感受一下多态所带来的魅力吧:maple_leaf:

1、几何体的体积计算

整体代码展示

//构造一个抽象几何形状类 —— 实现不同子类几何形状面积的求解public abstract class Gemotrey {    public abstract double getArea();}
复制代码


//柱类 —— 面向抽象类Gemotrey,为具体底面几何图形提供总抽象类接口public class Pillar {    Gemotrey bottom;        //底面几何图形对象    double height;          //柱体的高
//传入具体的底面几何图形和柱体的高 public Pillar(Gemotrey bottom, double height) { this.bottom = bottom; this.height = height; }
//对外获取柱体体积 public double getVolume(){ if(bottom == null){ System.out.println("没有底,无法计算面积"); return -1; } return bottom.getArea() * height; //通过具体的几何图形去重写抽象父类的获取面积方法 }}
复制代码


//圆类,继承自抽象类Gemotreypublic class Circle extends Gemotrey{    double r;
public Circle(double r) { this.r = r; }
@Override public double getArea() { return 3.14 * r * r; }}
复制代码


//矩形类,继承自抽象类Gemotreypublic class Rectangle extends Gemotrey{    int a,b;
public Rectangle(int a, int b) { this.a = a; this.b = b; }
@Override public double getArea() { return a * b; }}
复制代码


//测试类public class test {    public static void main(String[] args) {        Gemotrey bottom;    //几何形状底面对象        Pillar pillar;      //柱类对象        int height = 50;
//1.无底的对象 bottom = null; pillar = new Pillar(bottom,height); System.out.println("-----------"); System.out.println("无底的对象面积为:" + pillar.getVolume());
//2.圆形底对象 bottom = new Circle(10); pillar = new Pillar(bottom,height); System.out.println("-----------"); System.out.println("圆形底对象面积为:" + pillar.getVolume());
//3.矩形底对象 bottom = new Rectangle(20,15); pillar = new Pillar(bottom,height); System.out.println("-----------"); System.out.println("矩形底对象为:" + pillar.getVolume()); }}
复制代码


详细分析

  • 对于上述求求解几何体的体积,是多态的众多案例中非常经典的一道,因此我拿出来做讲解

  • 我们知道,要求一个柱体的体积,则要知道其底面面积和高,对于高,就是一个具体的数字,相乘即可,但是对于底面,却是【千奇百怪】,有圆底、方底、三角底甚至是椭圆底,那对于不同的形状类,我们要怎么去分别计算其底面积呢

  • 在没学习多态之前,相信大家一定都是这样做的,每一个底面都定义成一个类,然后这个类中有它们各自的计算面积的方法,在主方法中传入对应的底面边长和高,然后去调用每个几何类中的 getArea()方法,但是大家有没有发现这个 getArea()方法是大家都会用到的,而且如果你要增加一种柱体,还要再重新写这个 getArea()方法,需要大幅度得改动代码

  • 这时就需要用到我们多态的思想,可以将这个 getArea()方法==封装成一个几何体抽象类==,将这个 getArea()方法设置为抽象方法,然后每一个几何形状类去继承这个类,重写他们各自获取面积的方法,但是这还不够,因为你不知道此时是哪个柱体,因为你需要在定义一个柱体类,在这个柱体类中声明一个抽象类 Geometry 的对象作为其成员变量,然后在主方法中分别传入对应几何形状类的对象即可,这就可以使得不同的柱体所获取到的底面是不同的,而去调用不同类的获取面积的方法,形成了一个上转型对象,也自然地体现了多态的思想

2、手机里的 SIM 卡

整体代码展示

public abstract class SIM {    public abstract void setNumber(String number);       //设置电话号码    public abstract String getNumber();         //获取电话号码    public abstract String getCoreName();       //获取电话卡公司}
复制代码


public class MobileOfTelecom extends SIM{    String number;    @Override    public void setNumber(String number) {        this.number = number;    }
@Override public String getNumber() { return number; }
@Override public String getCoreName() { return "中国电信"; }}
复制代码


public class SIMOfChinaMobile extends SIM{    String number;    @Override    public void setNumber(String number) {        this.number = number;    }
@Override public String getNumber() { return number; }
@Override public String getCoreName() { return "中国移动"; }}
复制代码


public class SIMOfChinaUNnicom extends SIM{    String number;    @Override    public void setNumber(String number) {        this.number = number;    }
@Override public String getNumber() { return number; }
@Override public String getCoreName() { return "中国联通"; }}
复制代码


public class MobilePhone {    SIM sim;    public void useISM(SIM sim){        this.sim = sim;    }
public void showMess(){ System.out.println("您使用的手机号码是:" + sim.getNumber()); System.out.println("您使用的手机卡公司是:" + sim.getCoreName()); }}
复制代码


public static void main(String[] args) {    SIM sim;           //SIM电话卡类    MobilePhone mobilePhone = new MobilePhone();        //移动电话类
//1.中国电信 sim = new MobileOfTelecom(); sim.setNumber("18958473306"); //设置电话号码 mobilePhone.useISM(sim); //传入移动电话卡 mobilePhone.showMess(); //显示信息 System.out.println("------------");
//2.中国移动 sim = new SIMOfChinaMobile(); sim.setNumber("13955348743"); mobilePhone.useISM(sim); mobilePhone.showMess(); System.out.println("------------");
//3.中国联通 sim = new SIMOfChinaUNnicom(); sim.setNumber("13284835562"); mobilePhone.useISM(sim); mobilePhone.showMess();}
复制代码


详细分析

  • 手机我们每个人都有,每部手机里都会有这个 SIM 卡,就是电话卡的意思,我国的因特网服务提供商(ISP)主要有电信、联通、和移动三家,本人使用的是中国电信,但是网速还是比较快的,就是稍微有点贵,每个月

  • 好了,不扯开去,继续来讲题目,对手机卡,我们每个人的手机里可能不止一张,现在很多手机都是双卡双待的,但是大家有没有想过若是往手机里塞了两种不同的卡,那么这个手机要如何==识别这是哪家服务商所提供的卡==然后去存入相关的信息呢

  • 这其实也是要涉及到一个多态的思想,因为我们需要显示的是这些 SIM 卡的信息,但是这些信息是每张卡里都有的内容,所以我们可以将这个==SIM 卡设置成一个抽象类==,在里面定义三个抽象方法,分别是设置电话号码、显示电话号码以及显示其所在公司,然后分别在三家公司类中分别重写这三个方法。然后最重要的一点就是我们要如何去通过一个接收器去接收这张卡呢,我们可以把手机 MobilePhone 定义成一个类,然后去接收这张 SIM 卡,然后定义一个方法根据不同的电话厂商显示不同的信息

  • 这是多态中上转型对象在方法的形参中所出现的情况,通过传入子类的对象作为实参,然后将这个对象传给父类的引用,就实现了上转型对象继而体现了多态

六、【开-闭】原则(延伸拓展)

所谓的==开-闭原则(Open-Closed Principle)==,就是让设计的系统对扩展开发,对修改关闭


  • 当我们去设计一个系统的时候,这个系统肯定是需要【易维护】的,因此遵循【开-闭原则】是必须的。一个系统首先一点就是要满足用户的需求,将应对用户变化的部分设计为对扩展开放,而设计的核心部分是经过精心考虑之后确定下来的基本结构,这部分应当是对修改关闭的


我们通过一张框架图来具体理解一下



  • 这个是我在上一个版块讲到的【几何体的体积计算】这个案例,我们可以把其中的这些类看做一个开发框架,将 test 测试类看做是用户使用的应用程序,对于框架的核心部分,也就是抽象类 Geometry 和对应的接口柱类 Pillar 作为整个框架的核心部分,是经过精心考虑后设计出来的,最好是不要去轻易改动,因此我用的红色标记,而对于一些底面几何图形,它们应该是作为整个框架的可扩展部分,因此我用的绿色标记,当用户觉得当前的柱形体不够了,想要再加一个三角柱体,这个时候就可以作出相应的简易扩展,在这个框架中我们只需要增加一个 Geometry 的子类 Triangle,而无需去修改框架中的其他类

  • 在许多大厂:阿里、华为、百度等等...他们在开发一个用户程序的时候都会遵循一个【开-闭】原则,让整个程序变得很有弹性,变得易于维护,这既能增加用户的体验感,也能提高开发的效率。但是在现实的开发中我们其实是很难去遵循这个原则的,不经意间就修改了自己原本的代码,这就需要在开发一个系统框架前就设计构思好哪一块是可修改的,哪一块是不可轻易触碰的:mushroom:

七、总结与提炼

本文,我们重点讲解了==面向对象的第三大特征——多态==,讲到了多态存在所必不可少的一块,也就是实现上转型对象,然后说道了继承和多态之间的关系,初步地了解了多态的概念,接着来到了主题部分,也就是关键 abstract 的讲解,继而引申出了抽象类和抽象方法,了解了这些之后呢,我们又形成了一个面向抽象的编程思维,在实战案例中感受到了如如何运用这种思维去实现一个多态,最后我们讲到了企业开发中所需要遵循的【开-闭】原则,提到了何处需要,何处又需要对多态也有了一个完整的思维体系:deciduous_tree:


好了,这就是本文要讲述的所有内容,感谢您对本文的观看,如果错误请于评论区或私信指出:cherry_blossom:


发布于: 17 小时前阅读数: 10
用户头像

Fire_Shield

关注

语言观决定世界观 2022.09.02 加入

高校学生,热爱编程,喜欢写作

评论

发布
暂无评论
Java | abstract关键字【面向对象的第三大特征——多态】_Java_Fire_Shield_InfoQ写作社区