java经典面试题 --【面向对象的多态性】深入理解与应用

🚀 个人简介:某大型国企资深软件开发工程师,信息系统项目管理师、CSDN优质创作者、阿里云专家博主,华为云云享专家,分享前端后端相关技术与工作常见问题~

💟 作    者:码喽的自我修养🥰
📝 专    栏:Java入门到实战 🎉

🌈 创作不易,如果能帮助到带大家,欢迎 收藏+关注  💕

🌈🌈文章目录

一、多态的形式和体现

1. 对象的多态性

2. 多态的理解

3. 举例

1、方法内局部变量的赋值体现多态

2、方法的形参声明体现多态

3、方法返回值类型体现多态

二、多态的好处和弊端

三、虚方法调用(Virtual Method Invocation)

1. 成员方法具有多态性

2. 成员变量没有多态性

四、向上转型与向下转型

1. 为什么要类型转换

2. 如何向上或向下转型

3. instanceof关键字

4. 继承方法与继承属性的区别

一、多态的形式和体现

1. 对象的多态性

多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象

格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)

父类类型 变量名 = 子类对象;

举例:

Person p = new Student();

Object o = new Person();//Object类型的变量o,指向Person类型的对象

o = new Student(); //Object类型的变量o,指向Student类型的对象

对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象

2. 多态的理解

Java引用变量有两个类型:编译时类型运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译时,看左边;运行时,看右边。

  • 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)

  • 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法) “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法

多态的使用前提:① 类的继承关系 ② 方法的重写

3. 举例

先声明一个父类Pet

package com.atguigu.polymorphism.grammar;

public class Pet {
    private String nickname; //昵称

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public void eat(){
        System.out.println(nickname + "吃东西");
    }
}

声明第一个子类Cat

package com.atguigu.polymorphism.grammar;

public class Cat extends Pet {
    //子类重写父类的方法
    @Override
    public void eat() {
        System.out.println("猫咪" + getNickname() + "吃鱼仔");
    }

    //子类扩展的方法
    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}

声明第二个子类Dog

package com.atguigu.polymorphism.grammar;

public class Dog extends Pet {
    //子类重写父类的方法
    @Override
    public void eat() {
        System.out.println("狗子" + getNickname() + "啃骨头");
    }

    //子类扩展的方法
    public void watchHouse() {
        System.out.println("看家");
    }
}
1、方法内局部变量的赋值体现多态
package com.atguigu.polymorphism.grammar;

public class TestPet {
    public static void main(String[] args) {
        //多态引用
        Pet pet = new Dog();
        pet.setNickname("小白");

        //多态的表现形式
        /*
        编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
        运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
         */
        pet.eat();//运行时执行子类Dog重写的方法
//      pet.watchHouse();//不能调用Dog子类扩展的方法

        pet = new Cat();
        pet.setNickname("雪球");
        pet.eat();//运行时执行子类Cat重写的方法
    }
}
2、方法的形参声明体现多态
package com.atguigu.polymorphism.grammar;

public class Person{
    private Pet pet;
    public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
        this.pet = pet;
    }
    public void feed(){
        pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
    }
}
package com.atguigu.polymorphism.grammar;

public class TestPerson {
    public static void main(String[] args) {
        Person person = new Person();

        Dog dog = new Dog();
        dog.setNickname("小白");
        person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
        person.feed();

        Cat cat = new Cat();
        cat.setNickname("雪球");
        person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
        person.feed();
    }
}
3、方法返回值类型体现多态
package com.atguigu.polymorphism.grammar;

public class PetShop {
    //返回值类型是父类类型,实际返回的是子类对象
    public Pet sale(String type){
        switch (type){
            case "Dog":
                return new Dog();
            case "Cat":
                return new Cat();
        }
        return null;
    }
}
package com.atguigu.polymorphism.grammar;

public class TestPetShop {
    public static void main(String[] args) {
        PetShop shop = new PetShop();

        Pet dog = shop.sale("Dog");
        dog.setNickname("小白");
        dog.eat();

        Pet cat = shop.sale("Cat");
        cat.setNickname("雪球");
        cat.eat();
    }
}

二、多态的好处和弊端

好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。

弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的特有的属性和方法。

Student m = new Student();
m.school = "pku"; 	//合法,Student类有school成员变量
Person e = new Student(); 
e.school = "pku";	//非法,Person类没有school成员变量

// 属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。

开发中:

使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。

开闭原则OCP

  • 对扩展开放,对修改关闭

  • 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能

三、虚方法调用(Virtual Method Invocation)

1. 成员方法具有多态性

在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。

Person e = new Student();
e.getInfo();	//调用Student类的getInfo()方法

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

举例:

前提:Person类中定义了welcome()方法,各个子类重写了welcome()。

执行:多态的情况下,调用对象的welcome()方法,实际执行的是子类重写的方法。

拓展:

静态链接(或早起绑定):当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。那么调用这样的方法,就称为非虚方法调用。比如调用静态方法、私有方法、final方法、父类构造器、本类重载构造器等。

动态链接(或晚期绑定):如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)

2. 成员变量没有多态性

  • 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。

  • 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量

package com.atguigu.polymorphism.grammar;

public class TestVariable {
    public static void main(String[] args) {
        Base b = new Sub();
        System.out.println(b.a);
        System.out.println(((Sub)b).a);

        Sub s = new Sub();
        System.out.println(s.a);
        System.out.println(((Base)s).a);
    }
}
class Base{
    int a = 1;
}
class Sub extends Base{
    int a = 2;
}

四、向上转型与向下转型

首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。

1. 为什么要类型转换

因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。

但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过

  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型

    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了

    • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。

    • 此时,一定是安全的,而且也是自动完成的

  • 向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型

    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了

    • 但是,运行时,仍然是对象本身的类型

    • 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断

2. 如何向上或向下转型

向上转型:自动完成

向下转型:(子类类型)父类变量

package com.atguigu.polymorphism.grammar;

public class ClassCastTest {
    public static void main(String[] args) {
        //没有类型转换
        Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog

        //向上转型
        Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
        pet.setNickname("小白");
        pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
//        pet.watchHouse();//不能调用父类没有的方法watchHouse

        Dog d = (Dog) pet;
        System.out.println("d.nickname = " + d.getNickname());
        d.eat();//可以调用eat方法
        d.watchHouse();//可以调用子类扩展的方法watchHouse

        Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
        //但是这句代码会运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
    }
}

3. instanceof关键字

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。如下代码格式:

//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A 
  • 说明:

    • 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。

    • 如果对象a属于类A的子类B,a instanceof A值也为true。

    • 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。

代码:

package com.atguigu.polymorphism.grammar;

public class TestInstanceof {
    public static void main(String[] args) {
        Pet[] pets = new Pet[2];
        pets[0] = new Dog();//多态引用
        pets[0].setNickname("小白");
        pets[1] = new Cat();//多态引用
        pets[1].setNickname("雪球");

        for (int i = 0; i < pets.length; i++) {
            pets[i].eat();

            if(pets[i] instanceof Dog){
                Dog dog = (Dog) pets[i];
                dog.watchHouse();
            }else if(pets[i] instanceof Cat){
                Cat cat = (Cat) pets[i];
                cat.catchMouse();
            }
        }
    }
}

4. 继承方法与继承属性的区别

最后用一个小练习来体现在多态情况下,继承方法与继承属性的区别

class Base {
    int count = 10;
    public void display() {
        System.out.println(this.count);
    }
}

class Sub extends Base {
    int count = 20;
    public void display() {
        System.out.println(this.count);
    }
}

public class FieldMethodTest {
    public static void main(String[] args){
        Sub s = new Sub();
        System.out.println(s.count); //20
        s.display(); //20
        Base b = s;
        System.out.println(b == s); //true
        System.out.println(b.count); //10
        b.display(); //20
    }
}

到此这篇文章就介绍到这了,更多精彩内容请关注本人以前的文章或继续浏览下面的文章,创作不易,如果能帮助到大家,希望大家点点收藏+关注~💕 

    更多专栏订阅推荐:

🥕 JavaScript深入研究

👍 前端工程搭建
💕 vue从基础到起飞

✈️ HTML5与CSS3

🖼️ JavaScript基础

⭐️ uniapp与微信小程序

📝 前端工作常见问题与避坑指南

✍️ GIS地图与大数据可视化

📚 常用组件库与实用工具

💡 java入门到实战 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

码喽的自我修养

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

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

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

打赏作者

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

抵扣说明:

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

余额充值