JavaSE(9)-细节狂魔:OOP之继承多态?20K字长篇看完,有手就行

目录

 

🕛前言

​1.🕧继承

1.1🕐为什么需要继承

1.2🕜什么是继承

1.3🕑继承的语法

1.4🕝父类成员访问

1.4.1🕝子类中访问父类的成员变量

​1.4.2🕝子类中访问父类的成员方法

1.5🕒super关键字

1.5.1🕒super.成员变量

​1.5.2🕒super.成员方法

​1.5.3🕒super() 

​1.5.4🕒关于super的注意事项

1.6🕞super和this对比

【相同点】

【不同点】

1.7🕓检验你对super的理解

1.8🕟浅谈初始化

1.9🕔protected 关键字

1.10🕠继承方式

1.11🕕final关键字

1.12🕡继承与组合

2.🕖多态

2.1🕢多态的概念

2.2🕗多态实现条件

2.3🕣向上转型和向下转型

2.3.1🕣向上转型

2.3.2🕣向下转型

2.4🕘方法重写

2.4.1🕘引入重写

​2.4.2🕘重写的概念

2.4.3🕘重写的规则

​2.4.4🕘IDEA中的重写快捷键

​2.4.5🕘重写和重载的区别

2.4.6🕘动态绑定与静态绑定

2.5🕤实现多态

2.6🕙多态的三大前提

2.7🕥深入理解多态

2.8🕚多态的优缺点

2.8.1🕚使用多态的好处

2.8.2🕚多态缺陷

2.9🕦避免在构造方法中调用重写的方法

🕛尾声 

 

🕛前言

好久没写blog了,上一篇类和对象,我总共花了两天时间整理,第一次上了榜一。

我思索了半晌,回想起以前水的Blog,我大抵是明白了一个道理:精心琢磨过的Blog才有它的价值,收藏量也就显示了它的价值所在。于是便认真的花费了两天半时间敲下了这篇20k字的长篇-继承多态,欢迎各位食用。

我以后会努力更新尽量优质的内容,尽量做到详细,深入,通透,尽量周更。

欢迎uu们点个关注,我们一起进步。

如果你觉得本文对你有帮助的话,可以点个免费的收藏点赞评论嘛,这对我的鼓励真的很大。

≯(๑° . °๑)≮

我只是个初学Java的小白,水平有限,如果内容有误,欢迎大佬在评论区指正。

还有我感觉我排版好乱啊,排版方面如果有建议的话欢迎uu们在评论区留言呀。 不多废话啦,开始食用吧!

我们在上篇已经学习了OOP(面向对象)三大核心思想的封装了,本篇介绍继承和多态,非常重要,本篇共废时三天整理,干货满满,建议收藏。

1.🕧继承

1.1🕐为什么需要继承

在Java中,我们抽象出现实世界中实体具有的属性(成员变量)和功能(方法),可以创建模板(类)。
而类在实例化之后的产物(对象),则可以用来表示现实中的实体。
但是现实世界错综复杂,事物之间可能会存在一些关联,一旦有了关联,则可能会出现大量重复的代码,导致代码的冗余。

比如:我女朋友和猪,它们都有一个共性—都是动物。


我们通过Java语言来描述它们,就可以设计出以下代码:


通过观察上述代码会发现,Girl和Pig类中存在大量重复的代码,如下所示,红框框柱的部分就是它们的共性:


那我们能否将这些共性抽取出来呢?

面向对象思想中提出了继承的概念,专门用来进行共性抽取,从而实现代码复用。

1.2🕜什么是继承

继承机制 继承是面向对象程序设计时,能够使代码达到复用的最重要的手段。 它允许程序员在保持原有的父类特性的基础上进行扩展,增加新功能。 这样产生的新的类,我们称为 派生类
继承主要解决的问题是:共性的抽取,实现代码复用。

例如:女朋友和猪都是动物,那么我们就可以将它们共性的内容进行抽取,然后采用继承的思想来达到代码复用。


上述图示中,Girl和Pig都继承了Animal类。

其中:Animal类称为父类/基类或超类,Dog和Cat可以称为Animal的子类/派生类。继承之后,子类可以复用父类中的成员,子类在实现时只需关心自己新增加的成员即可。

继承实际上是一个is-a的关系,即:Girl is a Animal,Pig is a Animal。

从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态(后文会讲到)。

1.3🕑继承的语法

在Java中如果要表示类之间的继承关系,需要借助 extends关键字 ,具体如下:
我们对1.1的例子使用继承方式重新设计,就会有如下代码

父类:Animal.java 👇

public class Animal {
    String name;
    int age;
    float weight;
    public void eat(){
        System.out.println(name + "正在吃");
    }
    public void sleep(){
        System.out.println(name + "正在睡");
    }
}

子类:Girl.java👇

public class Girl extends Animal{
    public void cry(){
        System.out.println("呜呜呜");
    }
}

子类:Pig.java👇

public class Pig extends Animal{
    public void oink(){
        System.out.println("哼唧哼唧");
    }
}

测试类:Test.java👇

public class Test {
    public static void main(String[] args) {
        Girl girl = new Girl();
// Girl类中并没有定义任何成员变量,name和age和weight属性肯定是从父类Animal中继承下来的
        System.out.println(girl.name);
        System.out.println(girl.age);
        System.out.println(girl.weight);
// girl对象所访问的eat()和sleep()方法也是从Animal中继承下来的
        girl.eat();
        girl.sleep();
        girl.cry();
    }
}

运行结果(由于没有赋初值,所以打印的都是成员变量对应的0值)👇


注意👻
1. 子类会将父类中的成员变量或者成员方法继承到子类中
2. 子类继承父类之后,必须新增子类自己特有的成员。虽说不加也不会报错,但这样就 不能体现出子类与父类的不同了,这样继承不就没有意义了嘛

1.4🕝父类成员访问

在继承体系中,子类将父类中的方法和字段全部继承下来了,那在子类中能否直接访问父类中继承下来的成员呢?我们分别来分析这样几种情况。


1.4.1🕝子类中访问父类的成员变量

1. 子类和父类不存在同名成员变量

在内存中的分布情况👇

这时并没有创建一个父类的对象,而是子类将父类的属性全部都继承下来了。


 2. 子类和父类成员变量同名

这次的代码就值得我们细细推敲一下了。

我们发现程序的运行结果分别是10,□,30。我们根据运行结果来思考:

首先,在子类Dervied中所访问的data1究竟是父类的还是子类的?我们单从打印的结果是无法得出结论的,因为data1=10这条语句无论访问父类的还是子类的data1,都会使得最终打印结果10。

那就继续看下一条,data2=20,运行后我们发现打印的结果是一个字符方块?而父类Base的data2是int型啊,子类的data2才是char类型,所以data2=20访问的必然是子类的。于是我们就可以得出结论:子类和父类中的成员变量同名时,优先访问子类。

如果你还有所怀疑,再去IEDA里面浅浅观察一下:

我们发现只有data3是高亮的,而data1和data2因为没有被使用到而失去了光泽。

我们再打个断点看看编译器的参数:

发现父类的data1和data2都还是默认0值,所以证明了我们的结论是正确的。 


3.总结

在子类方法中,或者通过子类对象访问成员变量时:

  • 如果访问的成员变量在子类中有,优先访问自己的成员变量
  • 如果访问的成员变量在子类中无,则去访问父类继承下来的,如果父类也没有定义,则编译报错。
  • 成员变量访问遵循就近原则,自己有就优先自己的,如果没有则向父类中找

1.4.2🕝子类中访问父类的成员方法

1. 成员方法名字不同

class Base {
    public void methodA(){
        System.out.println("Base中的methodA()");
    }
}
class Derived extends Base{
    public void methodB(){
        System.out.println("Derived中的methodB()方法");
    }
    public void methodC(){
        methodB(); // 访问子类自己的methodB()
        methodA(); // 访问父类继承的methodA()
// methodD(); // 编译失败,在整个继承体系中没有发现方法methodD()
    }
}
如果成员方法没有同名,在子类方法中或者通过子类对象访问方法的时候,优先访问自己的,自己没有时再到父类中找,如果父类中也没有定义则报错。

2. 成员方法名字相同

class Base {
    public void methodA(){
        System.out.println("Base中的methodA()");
    }
    public void methodB(){
        System.out.println("Base中的methodB()");
    }
}
class Derived extends Base{
    public void methodA(int a) {
        System.out.println("Derived中的method(int)方法");
    }
    @Override
    public void methodB(){
        System.out.println("Derived中的methodB()方法");
    }
}
public class Test{
    public static void main(String[] args) {
        Derived derived=new Derived();
        derived.methodA(); // 没有传参,访问父类中的methodA()
        derived.methodA(20); // 传递int参数,访问子类中的methodA(int)
        derived.methodB(); //访问子类重写的methodB
    }
}

运行结果👇

说明👻
  • 通过子类对象访问父类与子类中的不同名方法时,优先在子类中找,找到则访问,找不到就在父类中找,找到则访问,父类里面也找不到的话就编译报错。
  • 通过子类对象访问父类与子类的同名方法时,如果父类和子类同名方法参数列表不同,就构成重载。根据调用方法时传递的参数选择合适的方法访问,如果没有则报错。
  1. 重载的要求是:方法名相同,参数列表不同,返回值不作要求。那么,继承中也能发生重载吗?我们通过查阅Oracle官方给出的解释文档网站:The Java® Language Specification (oracle.com)

  2. 按住ctrl+F,搜索OverLoading,点击红框框柱的部分

  3. 点进去,我们就发现,全是英文,反正我是啥也看不懂

  4. 然后这个时候就可以打开我们的万能的百度翻译了:

  5. 于是我们抓住关键点:类中的两个方法,无论是继承还是声明,只要它们方法名字相同,且不构成重写,就能构成重载。也就是说:继承中也可以发生重载。

  • 如果父类和子类同名方法的原型一致,则构成重写(如上述代码中子类的methodB,就是重写了父类的methodB)。我们通过子类对象只能访问到子类重写的方法,父类的被重写的方法无法通过子类对象直接访问到。


那么,我们来思考一个问题:如果子类中存在与父类中同名的成员时,就近原则优先访问子类自己的成员。那我们如何在子类中访问父类的同名成员呢?这里就不得不说一下super关键字了。

1.5🕒super关键字

如果子类和父类中存在相同名称的成员,我们想在子类方法中访问父类同名成员时,该如何操作?直接访问是无法做到的。

Java提供了super关键字,该关键字主要作用:在子类方法中访问父类的成员。

1.5.1🕒super.成员变量

你可能有些困惑,this和super为什么打印的东西不一样呢?看完他们的访问权限你就恍然大悟了。super和this的访问权限如下所示:

this指代当前对象的引用,它既能访问子类也能访问父类,那根据就近原则,当然是优先访问子类自己的啦。

注意:我们并没有创建父类对象!只是子类继承了父类的成员。

1.5.2🕒super.成员方法

值得一提的是,当子类中重写了父类的方法时,也可以通过super关键字在子类中调用父类的方法:

1.5.3🕒super() 

super(可有可无的参数列表),意为通过super调用父类的构造方法。

父子父子,先有父再有子。

即:子类对象构造时,需要先调用父类构造方法,然后再执行子类的构造方法。


示例代码👇

这里我们发现,我们没有在子类中手动加构造方法,但程序没有报错。这是因为编译器自己会帮我们完成这些工作,会默认给我们生成一个无参的super()。那么,我们如何验证默认生成的super是无参的呢?只需要把Base的构造方法改成有参即可:

Base的构造方法改成有参以后,编译器默认生成的无参的super()就无法匹配上父类带参数的构造方法。而一旦没有完成对父类的构造方法的调用,程序就会报错。即使我们自己手动加上无参的super(),也无济于事:

编译器提示我们要传入一个int类型的参数,那我们就传入吧,类型匹配上了,然后代码就能正常运行了:

1.5.4🕒关于super的注意事项

  • super只能在非静态方法中使用
  • 在子类方法中,super用于访问父类的成员变量和方法
  • 网上所广为流传的:super代表父类对象的引用,这句话不是很准确。super只是让代码更易读,让程序员看见这个super知道这是在使用父类的东西了。
  • 在构造子类对象时候 ,先要调用父类的构造方法,然后再完成子类自己的构造
  • 在子类构造方法第一行默认有隐含的super()调用,即调用父类的无参的构造方法。即使你不写子类的构造方法,编译器也会默认生成像下面这样的代码。
    public 子类类名 () {
        super();
    }
  • 如果我们显式的指定父类构造方法是带有参数的,父类中就不会再默认生成无参的构造方法,如果我们此时没有给子类指定父类的构造方法,那么编译器仍然会给子类生成默认的无参构造方法,而无参无法匹配有参的构造方法,此时就需要用户为子类显式定义构造方法,并在子类构造方法中匹配合适的父类构造方法调用,否则编译失败。这话听着有点绕,多看几遍也就理解了。
  • 在子类的构造方法中,super(...)必须放在第一行。
  • super(...)只能在子类构造方法中出现一次,并且不能和this同时出现

1.6🕞super和this对比

相同点

  • 都是Java中的关键字
  • 都只能在类的非静态方法中使用,用来访问非静态成员方法和字段
  • 在构造方法调用的时候,只能放在构造方法中的第一条语句,并且super()和this()不能同时存在于同一个构造方法。

不同点

  • this指代当前对象的引用,当前对象即调用这个实例方法的对象。而super相当于是子类对象中从父类继承下来部分成员的引用
  • 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
  • this是非静态成员方法的一个隐藏参数,super不是隐藏的参数
  • 成员方法中直接访问本类成员时,编译之后会将this还原,即本类非静态成员都是通过this来访问的;在子类中如果通过super访问父类成员,编译之后在字节码层面super实际是不存在的(不理解问题也不大)
  • 在构造方法中:this(...)用于调用本类构造方法,super(...)用于调用父类构造方法,两种调用不能同时在构造方法中出现
  • 在子类的构造方法中一定会存在super(...)的调用,用户没有写编译器也会自欧东生成,但是this(...)用户不写则没有

1.7🕓检验你对super的理解

能够理清下面这段代码说明对super已经基本理解了,浅试一下吧。

class Base {
    int a;
    int b;
    public void methodA(){
        System.out.println("Base中的methodA()");
    }
    public void methodB(){
        System.out.println("Base中的methodB()");
    }
}
class Derived extends Base{
    int a; // 与父类中成员变量同名且类型相同
    char b; // 与父类中成员变量同名但类型不同
    // 与父类中methodA()构成重载
    public void methodA(int a) {
        System.out.println("Derived中的method()方法");
    }
    // 与基类中methodB()构成重写(即原型一致,重写后序详细介绍)
    public void methodB(){
        System.out.println("Derived中的methodB()方法");
    }
    public void methodC(){
// 对于同名的成员变量,直接访问时,访问的都是子类的
        a = 100; // 等价于: this.a = 100;
        b = 101; // 等价于: this.b = 101;
// 注意:this是当前对象的引用
// 访问父类的成员变量时,需要借助super关键字
// super是获取到子类对象中从基类继承下来的部分
        super.a = 200;
        super.b = 201;
// 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
        methodA(); // 没有传参,访问父类中的methodA()
        methodA(20); // 传递int参数,访问子类中的methodA(int)
// 如果在子类中要访问重写的基类方法,则需要借助super关键字
        methodB(); // 直接访问,则永远访问到的都是子类中的methodA(),基类的无法访问到
        super.methodB(); // 访问基类的methodB()
    }
}
public class Test{
    public static void main(String[] args) {
        Derived derived=new Derived();
        derived.methodC();
    }
}

运行结果👇

1.8🕟浅谈初始化

在类和对象篇章中,我们提到了代码块的概念和用法,比较重要的有实例代码块和静态代码块。我们简单回顾一下它们在没有继承关系时的执行顺序:

class Person {
    public String name;
    public int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("构造方法执行");
    }

    {
        System.out.println("实例代码块执行");
    }

    static {
        System.out.println("静态代码块执行");
    }
}

public class Test{
    public static void main(String[] args) {
        Person person1 = new Person("熊大",8);
        System.out.println("============================");
        Person person2 = new Person("熊二",10);
    }
}

运行结果👇

  • 静态代码块最先执行,并且只执行一次,且是在类加载的阶段执行的,不管创没创建对象,只要加载了类,就会自动执行。
  • 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,再然后就是构造方法执行了

继承关系时的执行顺序

class Person {
    public String name;
    public int age;
    //—————————————————————————————————————————————————————————————————————————————//
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("父类Person:构造方法执行");
    }
    //—————————————————————————————————————————————————————————————————————————————//
    {
        System.out.println("父类Person:实例代码块执行");
    }
    //—————————————————————————————————————————————————————————————————————————————//
    static {
        System.out.println("父类Person:静态代码块执行");
    }
//—————————————————————————————————————————————————————————————————————————————//
}

class Student extends Person{
    //—————————————————————————————————————————————————————————————————————————————//
    public Student(String name,int age) {
        super(name,age);
        System.out.println("子类Student:构造方法执行");
    }
    //—————————————————————————————————————————————————————————————————————————————//
    {
        System.out.println("子类Student:实例代码块执行");
    }
    //—————————————————————————————————————————————————————————————————————————————//
    static {
        System.out.println("子类Student:静态代码块执行");
    }
//—————————————————————————————————————————————————————————————————————————————//
}

public class Test {
    public static void main1(String[] args) {
        Person person1 = new Person("熊大", 8);
        System.out.println("============================");
        Person person2 = new Person("熊大", 8);
    }
    public static void main2(String[] args) {
        Student student1 = new Student("熊大",8);
        System.out.println("===========================");
        Student student2 = new Student("熊二",10);
    }
}

 main1运行结果👇

main2运行结果👇 

通过分析执行结果,得出以下结论:

  • 父类静态代码块优先于子类静态代码块执行,且是最早执行
  • 父类实例代码块和父类构造方法紧接着执行
  • 子类的实例代码块和子类构造方法紧接着再执行
  • 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行

1.9🕔protected 关键字

在类和对象篇中,为了实现封装特性,Java中引入了访问限定符。
访问限定符主要限定:类或者类中的成员能否在类外或者其他包中被访问。


那父类中不同访问权限的成员,在子类中的可见性又是什么样子的呢?我们就此探讨一下:

先看接下来要创建的包以及类的结构👇


在包extend01创建Base父类👇

package extend01;
public class Base {
    private int a;
    int b;
    protected int c;
    public int d;
}

在包extend01创建Derived1子类👇

package extend01;
// extend01包中
// 这是同一个包中Base的子类
public class Derived1 extends Base{
    public void method(){
// super.a = 10; // 编译报错,父类private成员在相同包的子类中不可见
        super.b = 20;// 父类中默认访问权限修饰的成员在相同包子类中可以直接访问
        super.c = 30; // 父类中public成员在相同包子类中可以直接访问
        super.d = 40; // 父类中protected成员在相同包子类中可以直接访问
    }
}

Derived1中的成员访问情况👇


在包extend02中创建Derived2子类👇

package extend02;
import extend01.Base;
// extend02包中
// 不同包中Base的子类
public class Derived2 extends Base {
    public void method(){
         //super.a = 10; 编译报错,父类中private成员在不同包子类中不可见
        //super.b = 20; 父类中默认访问权限修饰的成员在不同包子类中不能直接访问
        super.c = 30; //父类中protected修饰的成员在不同包的子类中可以直接访问
        super.d = 20; //父类中public修饰的成员在不同包子类中可以直接访问
    }
}

Derived2中的成员访问情况👇


在包extend02中创建Test测试类👇

package extend02;
// extend02包中
// 不同包中的类
public class Test {
    public static void main(String[] args) {
        Derived2 test = new Derived2();
        test.method();
        //System.out.println(test.a);//编译报错,父类中private成员在不同包其他类中不可见
        //System.out.println(test.b);//父类中默认访问权限修饰的成员在不同包其他类中不能直接访问
        //System.out.println(test.c);//父类中protected成员在不同包其他类中不能直接访问
        System.out.println(test.d); //父类中public成员在不同包其他类中可以直接访问
    }
}

Test中的成员访问情况👇

值得一提的是,父类中private成员变量虽然在子类中不能直接访问,但是也继承到子类中了,我们给父类提供public的getter and setter接口即可访问。

 总结👻

  • protected又称继承权限,既可以在同一个包的同一个类使用,也可以在同一个包的不同类使用,还可以在不同包里的子类(继承),而在不同包里面的不同类就不能使用了。
  • 我们希望类要尽量做到 "封装",即隐藏内部实现细节,只暴露出必要的信息给类的调用者。因此我们在使用的时候应该尽可能的使用比较严格的访问权限。例如如果一个方法能用 private,就尽量不要用public。我们主要还是得根据业务需求来设计程序。

1.10🕠继承方式

在现实生活中,事物之间的关系是非常复杂,灵活多样,例如:

但在Java中只支持以下几种继承方式:

注意:Java中不支持多继承

我们在实际开发中所遇到的项目往往业务比较复杂,即使如此, 我们也不希望类之间的继承层次太复杂。 一般尽量不要出现超过三层的继承关系。
如果继承层次太多, 就需要考虑对代码进行重新实现了。
如果想从语法上对继承进行限制, 就可以使用final关键字

1.11🕕final关键字

final关键字可以用来修饰变量、成员方法以及类。


1. 修饰变量或字段,表示常量(即不能修改)

final int a = 10; 
a = 20; // 常量不可修改,编译出错
2. 修饰类:表示此类不能被继承,被修饰的类也叫密封类
final public class Animal {
...
}
public class Pig extends Animal {
...
}// 编译出错 Error: java: 无法从最终com.bit.Animal进行继承
我们平时是用的String字符串类,就是用final修饰的,表示不能被继承
3. 修饰方法:表示该方法不能被重写(多态部分介绍)

1.12🕡继承与组合

和继承类似,组合也是一种表达类之间关系的方式,也能够达到代码复用的效果。
组合并没有涉及到特殊的语法,它仅仅是将一个类的实例作为另外一个类的字段。

继承表示对象之间是is-a的关系。比如:狗是一个动物,猫是一个动物

组合表示对象之间是has-a的关系。 比如学校类里面有多个学生的实例化对象和多个老师实例化对象作为字段,学生和老师都有实例化的钱对象作为字段(此有钱非彼有钱),钱对象里面又有count数量作为字段,这就是has-a的关系。has-a就是由...构成
/**
 * 金钱类
 */
class Money {
    int count;
}

/**
 * 学生类
 */
class Student {
    public Money money;
}

/**
 * 老师类
 */
class Teacher {
    public Money money;
}

/**
 * 学校类
 */
class School {
    public Student[] students;
    public Teacher[] teachers;
}

组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。

2.🕖多态

2.1🕢多态的概念

多态是OOP三大核心思想之一,封装,继承,多态。
通俗来说,多态就是多种形态。
具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
比如:不同的电视机,使用同样的功能-播放视频,黑白电视机放的动画是黑白的,彩色电视机则是彩色的,它们完成的动作都是播放,但表现的形式却不同了,这就是一种多态。

总的来说:同个方法,不同对象去使用它,就会产生不同的结果。这就是多态的思想

2.2🕗多态实现条件

在java中要实现多态,必须要满足如下三个条件,缺一不可:
  • 必须在继承体系下
  • 子类必须要对父类中方法进行重写
  • 向上转型,通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同子类对象给父类引用时,会调用它们对应类中重写的方法。

我们已经学习了继承,还需要学习重写和向上转型,只有理解了这三个条件,才能真正理解多态思想。我们先学习向上转型和向下转型。

2.3🕣向上转型和向下转型

2.3.1🕣向上转型

了解向上转型的基本概念前,我们先来看这样一段代码:

class Animal {
    public String name;
    public int age;
    public void eat() {
        System.out.println(this.name+"正在吃~~~");
    }
}

class Cat extends Animal {
    public String hair;
    public void mew() {
        System.out.println(this.name+"正在喵喵叫~~~");
    }
}

public class Test {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.mew();
        cat.eat();
    }
}

我们知道,子类cat对象可以访问从父类中继承下来成员。

那么,如果我们通过父类对象访问子类?又是怎么样的呢?我们直接通过Animal类实例化一个animal对象,能够发现:

animal对象无法访问 在子类里面有定义,但在Animal类里面没有定义的成员。

有了这个知识铺垫,我们就可以开始引入向上转型了。


向上转型:实际就是将父类的引用指向子类对象,当成父类对象来使用。

在上面代码的基础上,我们new一个子类Cat,然后用父类类型的animal来接收,这就是向上转型。此时,父类的引用animal指向了子类对象Cat。

 Animal animal = new Cat();

animal是父类类型,但它可以引用子类对象。
因为这是从小范围向大范围的转换,大范围可以囊括小范围,所以这是安全的。
比如,猫是一个动物,动物的特性猫当然会有啦,所以Cat向Animal转型是合理的。

然后我们发现:animal的类型是Animal,此时它只能去访问类Animal的成员变量和方法,去访问子类Cat的成员变量或方法的时候会报错,这也是我们在引入向上转型前所提到的问题:


【总结】

向上转型将子类通过父类引用来接收了,那么,就只能去访问父类所定义了的成员。


使用场景

  • 直接赋值:通过赋值操作符,用父类引用直接接收子类对象。
    Animal animal = new Cat();
  • 方法传参:顾名思义就是作为作为方法的参数进行向上转型。
  • 方法返回:顾名思义就是作为方法返回值传递。
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。

2.3.2🕣向下转型

对于向下转型我们只需要知道有这个东西存在即可。
什么是向下转型呢?
我们将一个子类对象经过向上转型之后,我们就只能调用其父类的方法,再无法调用子类的方法,但有时候我们可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可。
子类的引用 接收 被强制转为子类类型的父类引用指向的对象,这就是向下转型

先看这样一段代码👇

我们发现animal对象是无法访问子类的fly的,因为fly是子类Bird的特有的方法,而在animal中没有给出定义。animal是Animal类型的,只能调用Animal里面的成员。


然后我们再看这样一段代码👇

我们的操作是先new一个Bird对象,使用Animal类型的animal引用来进行接收,发生了向上转型,此时的animal是不会飞的,我们只需要将animal还原为Bird即可。也就是通过强转,将Animal类型的animal强转为bird,由于最初传给animal的是Bird对象,本身就是可以飞的,这个操作只是还原,所以程序可以正常运行,能够调用Bird的fly()方法

但是,向下转型不安全,我们不建议使用向下转型,为什么说它不安全呢?其实也很好理解,比如,Bird对象可以用父类引用animal接收发生向上转型,然后这个animal引用再向下转型为Bird就可以正常还原。但如果是Cat对象用父类引用animal接收然后就会发生向上转型,然后通过animal引用再向下转型为Bird呢?那鸟和猫能一样嘛?猫能飞起来?所以说尽量少用向下转型。

将上述文字用代码实现就是这样的👇

程序一运行,就报错了。通过万能的百度翻译,我们可以知道报错部分大意是这样的:

“main”中出现异常。猫不能传递给鸟。


我们如果非要使用向下转型,需要确保其安全性,就要规避上面的问题了,我们需要确保传递的对象类型是兼容的。例如Bird类对象向上转型为Animal后,最后向下转型也应该还原为Bird。

示例代码如下👇

加上instanceof这段代码进行判断以后,代码就没有报错了。

那么,instanceof是做什么的呢?百度百科的解释是这样的:

instanceof是Java的保留关键字,它的作用是判断其左边的对象是否为其右边的类的实例,然后返回boolean类型的数据。

上面代码中instanceof就是判断animal引用的对象是不是Bird类的实例,如果是,说明能够安全的进行转型,如果不是,则不执行里面的语句。

2.4🕘方法重写

2.4.1🕘引入重写

想要理解多态,我们还差最后一个知识储备:方法重写

在引入方法重写之前,我们先回顾这样一段代码👇

class Animal {
    public String name;
    public int age;
    public void eat() {
        System.out.println(this.name+"正在吃~~~");
    }
}

class Cat extends Animal {
    public String hair;
    public void mew() {
        System.out.println(this.name+"正在喵喵叫~~~");
    }
}

class  Bird extends Animal{
    public void  fly(){
        System.out.println("正在飞~~~");
    }
}

我们知道,在现实世界中,猫是吃猫粮的,如果我们想修改子类Cat调用的eat方法的话,肯定不可以在父类Animal上面进行修改的,如果把父类Animal的eat改成吃猫粮,那子类Bird调用eat不也就变成了吃猫粮了吗?这显然不河狸好吧。

那么,如果想要实现子类Cat调用父类的eat方法就打印吃猫粮,子类Bird调用父类的eat方法就打印吃鸟粮的话,我们需要在这两个子类中分别重新去实现一遍eat方法。

这就是重写的操作👇

class Animal {
    public String name;
    public int age;
    public void eat() {
        System.out.println(this.name+"正在吃~~~");
    }
}

class Cat extends Animal {
    public String hair;
    public void mew() {
        System.out.println(this.name+"正在喵喵叫~~~");
    }

    @Override
//重写的eat方法
    public void eat() {
        System.out.println(this.name+"正在吃猫粮");
    }
}

class  Bird extends Animal{
    public void  fly(){
        System.out.println("正在飞~~~");
    }
    @Override
//重写的eat方法
    public void eat() {
        System.out.println(this.name+"正在吃鸟粮");
    }
}
public class Test{
    public static void main(String[] args) {
        Animal animal=new Animal();
        Cat cat=new Cat();
        Bird bird =new Bird();
        animal.eat();
        cat.eat();
        bird.eat();
    }
}

程序运行结果如下👇

在子类中重写了eat方法以后,子类对象调用eat方法,打印出来的结果就变成了子类重写实现的eat方法了。

2.4.2🕘重写的概念

重写,也称为覆盖。

重写是指 子类将父类非static、private、final修饰的方法实现过程进行重新编写,并且重写的方法返回值和形参列表都要和原来保持一致。即外壳不变,重写核心部分。

重写的好处就是,子类可以根据需要,定义特定属于自己的行为。 也就是说子类能够根据需要重新实现父类的方法。例如上面的猫吃猫粮,鸟吃鸟粮。

2.4.3🕘重写的规则

方法重写的三个前提:

  1. 方法名相同;
  2. 方法的返回值相同;
  3. 方法的参数列表相同。

当子类对方法进行重写以后,此时通过子类对象去调用eat方法,调用的实际上就是子类重写的eat方法了。

我们把这个现象叫做动态绑定(这是多态的基础)

我们来通过cmd的【javap -c 字节码文件名】指令查看一下上述代码编译生成的字节码文件的内容。

从字节码来看调用的实际上还是父类Animal的eat方法:

在运行的时候,才变成了子类Cat自己的eat方法;

因此,动态绑定又称为运行时绑定,只有在程序运行的时候才知道要调用谁。


总结重写的小规则👇

  • 子类在重写父类的方法时,一般必须与父类方法原型一致:返回值类型 方法名(参数列表) 要完全一致
  • 但在JDK7以后,被重写的方法返回值类型可以不同,但是返回值必须是具有父子关系的。这也叫做协变类型。
  • 子类重写的方法的访问权限不能比父类中被重写的方法的访问权限低。例如:如果父类方法被protected修饰,则子类中重写该方法就只能声明为protected或public,
  • 父类被static、private、final修饰的方法都不能被重写。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final,static的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非final,static修饰的方法。

2.4.4🕘IDEA中的重写快捷键

2.4.5🕘重写和重载的区别

如下表所示👇

2.4.6🕘动态绑定与静态绑定

静态绑定 也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
动态绑定 也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。

2.5🕤实现多态

示例代码👇

class Animal {
    public String name;
    public int age;
    public void eat() {
        System.out.println(this.name+"正在吃");
    }
}

class Cat extends Animal {
    public String hair;
    @Override
    public void eat(){
        System.out.println(this.name+"吃猫粮~~~");
    }
    public void mew() {
        System.out.println(this.name+"喵喵喵~~~");
    }
}

class Dog extends Animal {
    @Override
    public void eat(){
        System.out.println(this.name+"吃狗粮~~~");
    }
}
//---------------------------我是分割线呀-----------------------------------//
public class Test {
    public static void func(Animal animal) {
        animal.eat();
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        func(cat);
        func(dog);
    }
}

在上述代码中,分割线上方的代码是类的实现者,分割线下方的代码是类的调用者。

1.在这串代码中,首先Cat和Dog都继承了父类Animal。
2.然后Cat和Dog都重写了父类的eat方法。
3.在Test类中的func方法中,将父类作为形参,将子类对象作为传入func方法的实参,发生了向上转型。
这是多态的三个必要前提。
由于传入func方法的子类对象不同,所以调用同一个func方法时所表现出来的形式也不同,这就是多态。
运行结果如下👇

调用同一个方法,因为传入方法的引用(名词)所引用(动词)的对象是不同的,所以执行方法所表现出来的形为也是不同的,这种思想就叫多态。

2.6🕙多态的三大前提

  1. 发生向上转型:父类引用(名词)引用(动词)子类的对象。
  2. 发生重写:父类和子类当中有同名的覆盖方法。
  3. 通过父类引用,调用这个同名的方法,此时会发生动态绑定。

2.7🕥深入理解多态

我们现在想要画一个图形。

先创建一个图形类Shape👇

class Shape {
    public void draw(){
        System.out.println("画图形!!!!!!");
    }
}

但这只是说画图形,未免有些太单调,如果我们现在想画很多种具体的图形,那么就可以去重写Shape类里面的draw方法来满足自己的需求。

创建子类并重写draw方法👇

class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("●");
    }
}
 
class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("♦");
    }
}
 
class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("△");
    }
}

我们如果想要画出这些图形,就可以在测试类中实例化对象来调用draw方法👇

public class  Test{
    public static void drawMap(Shape shape) {
        shape.draw();
    }
    public static void main(String[] args) {
        Cycle cycle = new Cycle();
        Rectangle rectangle = new Rectangle();
        Triangle triangle = new Triangle();
        drawMap(cycle);
        drawMap(rectangle);
        drawMap(triangle);
    }
}

传入方法的引用对象不同,draw方法所表现的行为就不同,这就是多态。

程序运行结果👇

多态的可扩展能力非常强,比如说我们现在想画一朵花,只需要再创建一个Flower类然后重写draw方法即可。

class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("✿");
    }
}

在Test类中实例化Flower对象,调用drowMap方法

drawMap(new Flower());

程序运行结果👇

所以说,多态是非常灵活的。 

2.8🕚多态的优缺点

2.8.1🕚使用多态的好处

(1)能够降低代码的 "圈复杂度", 避免使用大量的 if - else

什么叫 "圈复杂度" :
圈复杂度是一种描述一段代码复杂程度的方式。
一段代码如果平铺直叙,那么就比较简单容易理解。
而如果有很多的条件分支或者循环语句,就认为理解起来更复杂。
我们可以通过简单粗暴的计算一段代码中条件语句和循环语句出现的个数,
这个个数就称 "圈复杂度"。
如果一个方法的圈复杂度太高, 就需要考虑重构。
不同公司对于代码的圈复杂度的规范不一样, 一般不会超过10。

例如我们现在需要打印多个形状。

如果不基于多态,代码实现如下👇

public class Test{
    public static void drawShapes() {
        Rectangle rect = new Rectangle();
        Cycle cycle = new Cycle();
        Flower flower = new Flower();
        String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
        for (String shape : shapes) {
            if (shape.equals("cycle")) {
                cycle.draw();
            } else if (shape.equals("rect")) {
                rect.draw();
            } else if (shape.equals("flower")) {
                flower.draw();
            }
        }
    }
}

那假设是要打印一百个、一千个不同的对象呢?难道我们写一百条,一千条判断语句吗?这显然不河狸吧。

如果使用使用多态,则不必写这么多的 if - else 分支语句,代码更简单。👇

public class  Test{
    public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
        Shape[] shapes = {new Cycle(), new Rectangle(), new Cycle(),
                new Rectangle(), new Flower()};
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}
(2)可扩展能力更强
如果要新增一种新的形状,使用多态的方式代码改动成本也比较低。对于类的调用者来说(drawShapes方法),只要创建一个新类的实例就可以了,改动成本很低。而对于不用多态的情况,就要把 drawShapes 中的 if - else 进行一定的修改,改动成本更高。


2.8.2🕚多态缺陷

代码的运行效率降低。

2.9🕦避免在构造方法中调用重写的方法

我们来看这样一段有坑的代码。

我们创建两个类,B是父类,D是子类。

D中重写了父类的func方法,并且在B的构造方法中调用了func方法:

class B {
    public B() {
        func();
    }
    public void func() {
        System.out.println("父类B的func方法被调用了");
    }
}
class D extends B {
    @Override
    public void func() {
        System.out.println("子类D的func方法被调用了");
    }
}
public class Test {
    public static void main(String[] args) {
        D d = new D();
    }
}

我们看到这代码,惯性思维,那肯定是就近原则啊,那肯定打印父类B的func方法嘛。

然而程序运行结果如下👇

蛙蚌住了。。。为什么会这样呢?其实还是细节问题:

构造 D 对象的同时,会先调用 B 的构造方法。
B的构造方法中调用了 func 方法,此时会触发动态绑定, 会调用到D中的func,此时D对象自身还没有构造。
结论: 尽量不要在构造器中调用方法(如果这个方法被子类重写了,就会触发动态绑定,但是此时子类对象还没构造完成),可能会出现一些隐藏的但是又极难发现的问题。

🕛尾声

ok啦xdm,看到这里相信你已经对继承多态熟透于心了。

如果这篇blog对你有帮助的话,可以点赞收藏关注评论和博主互动噢。这对我的鼓励真的很大。

如果你是一个相信改变的人,现在怎么样并不重要,你相信自己会变成什么样才重要。

如果再也不能见到你,也祝你早安,午安,还有晚安。

  • 104
    点赞
  • 83
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 160
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 160
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶轻衣。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值