【Java 类与对象】多态

空山新雨后

天气晚来秋


目录

多态的概念

多态实现条件

多态的转型

向上转型

向下转型

instanceof 关键字

方法的重写

@Override注解

重写的权限

只能重写继承而来的方法(1)

 final、static 不能被重写(2)

重写的方法不能带有等级更严格的修饰符(3) 

构造方法不能被重写(4)

在子类中通过 super 可以调用父类的重写(5)

重载与重写

静态绑定与动态绑定

动态绑定的概念:

静态绑定的概念:

多态的运用

多态在实际生活中的运用

多态在代码练习中的运用

能够降低代码的重复性

可扩展能力更强

多态的概念

多态的概念:

<1> 多态是继封装、继承之后,面向对象的第三大特性

<2> 通俗来说,多态就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

这里举个例子:

人吃东西是一个很广泛的概念,但是我们的学生与教师是不同的两种形态,当它们去吃饭的时候就有不同的待遇:教师可以吃教师餐,而学生只能吃学生餐 -- 这就是多态。而它们这种同名的动作(吃),但是实现的方法不一样(学生餐、教师餐)的操作就叫做 -- 重写

多态实现条件

在 java 中要实现多态,必须要满足如下几个条件,缺一不可:

<1> 必须在继承体系下
<2> 子类必须要对父类中方法进行重写
<3> 通过父类的引用调用重写的方法

class Person {
    protected String name;
    protected int age;
    protected int id;

    public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public void eat(){
        System.out.println("正在吃");
    }

}

class Student extends Person {

    public Student(String name, int age, int id) {
        super(name, age, id);
    }

    public void eat(){
        System.out.println(this.name + " 正在吃学生餐");
    }

}

class Teacher extends Person {

    public Teacher(String name, int age, int id) {
        super(name, age, id);
    }

    public void eat(){
        System.out.println(this.name + " 正在吃教师餐");
    }

}

class Test {
    public static void eat(Person a){
        a.eat();
    }
    public static void main(String[] args) {
            Student cat = new Student("张三",18,2024123);
            Teacher dog = new Teacher("李四", 38,2024567);
            eat(cat);
            eat(dog);
        System.out.println("--------------------------------------------");
            Person s2 = new Student("王五",18,2024234);
            s2.eat();
            Person t2 = new Teacher("赵六",38,2020789);
            t2.eat();
    }
}

多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法 

 我们的调用方法一般有两种:

 《1》

public static void eat(Person a){
        a.eat();
    }
    public static void main(String[] args) {
            Student cat = new Student("张三",18,2024123);
            Teacher dog = new Teacher("李四",38,2024567);
            eat(cat);
            eat(dog);
            //----------------------------------------------------------------
            //eat(new Student("张三",18,2024123));
            //eat(new Teacher("李四", 38,2024567));
    }

这种方法比较符合实际的开发,因为一个大型项目都不是一个人写出来的:一般是由一些人写类,一些人再写调用类的功能

再提多态: 

当类的调用者在编写 eat 这个方法的时候,参数类型为 Person (父类),此时在该方法内部并不知道,也不关注当前的 a 引用指向的是哪个子类的实例。此时 a 这个引用调用 eat 方法可能会有多种不同的表现(和 a 引用的实例相关),这种行为就称为多态

 《2》

    public static void main(String[] args) {
            Person s2 = new Student("王五",18,2024234);
            s2.eat();
            Person t2 = new Teacher("赵六",38,2020789);
            t2.eat();
    }

这个就涉及到多态的转型 -- 向上转型,我们接下来就来谈谈多态的转型

多态的转型

向上转型

本质:父类的引用指向子类的对象

特点:

编译类型看左边,运行类型看右边
可以调用父类的所有成员(需遵守访问权限)
不能调用子类的特有成员
运行效果看子类的具体实现
格式:
父类类型 引用名 = new 子类类型();

其他都好理解,我们这里来说一下第三点:不能调用子类的特有成员

class Person {
    protected String name;
    protected int age;
    protected int id;
    public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }
    public void eat(){
        System.out.println("正在吃");
    }
}

class Student extends Person {
    public String interest;
    public void interest(){
        System.out.println("游戏");
    }
    public Student(String name, int age, int id) {
        super(name, age, id);
    }
    public void eat(){
        System.out.println(this.name + " 正在吃学生餐");
    }
}


class Test {
    public static void eat(Person a){
        a.eat();
    }
    public static void main(String[] args) {
        Person s1 = new Student("王五",18,2024234);
        s1.eat();
        System.out.println(s1.interest);
        s1.interest();
    }
}

这里说一下我对这个的理解:我们 C语言 阶段不是学过类型转化吗?我们的 double 转化成 int 会损失精度,这里也一样:首先它们是继承关系,子类中拥有父类的所有成员属性,但是也有自己特有的属性。多态就是将子类与父类相同的属性 “分割” 给父类,这样我们调用父类的引用就能调用子类特有的方法,而父类中没有的属性则不能被调用(相当被丢失的精度)

向上转型的优点:让代码实现更简单灵活 
向上转型的缺陷:不能调用到子类特有的方法

向下转型

将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换 

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

class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }
    @Override
    public void eat(){
        System.out.println(this.name+" 吃狗粮");
    }
    public void bark(){
        System.out.println(this.name+" 旺旺叫");
    }
}

public class Test{
    public static void main(String[] args) {
        Animal a = new Dog("大黄",10);
        Dog g = (Dog)a;
        g.eat();
    }
}

注意:你不能把当前子类的父类引用给别的子类向下转型

 

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

class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }
    @Override
    public void eat(){
        System.out.println(this.name+" 吃狗粮");
    }
    public void bark(){
        System.out.println(this.name+" 旺旺叫");
    }
}

class Cat extends Animal{
    public Cat(String name, int age){
        super(name, age);
    }
    @Override
    public void eat(){
        System.out.println(this.name+"吃鱼");
    }
}

public class Test{
    public static void main(String[] args) {
        Animal a = new Dog("大黄",10);
        Cat c = (Cat)a;
        c.eat();
    }
}

虽然代码并没有报错但是编译的时候会错出异常:

这里解释一下:父类对象 a 引用了 Dog 的子类对象,但是拿着这个 Dog 引用的父类对象向下转型给了 Cat 的子类对象,猫不是狗!!!

instanceof 关键字

instanceof 运算符是 Java 中的一种类型判断运算符,用于检查一个对象是否是一个类的实例 ,如果是返回 ture ,不是则返回 false

所以向下转型是有一定风险的,所以我们在使用的时候需要再判断一下

向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java 中为了提高向下转型的安全性,引入 了 instanceof,如果该表达式为 true,则可以安全转换

Animal a = new Dog("大黄",10);
        if(a instanceof Cat){
            Cat c = (Cat)a;
            c.eat();
        }else{
            System.out.println("a instanceof Cat not!");
        }

 为了安全考虑,我们在使用向下转型时,都应该判断一下

 

方法的重写

重写的概念:

如果子类具有和父类一样的方法,我们称之为方法重写。 方法重写用于提供父类已经声明的方法的特殊实现,是实现多态的基础条件

重写的条件:

方法名相同
参数相同(个数、顺序、类型)、返回值类型相同
必须是继承关系

@Override注解

@Override 是重写的注解,其主要作用是检查该方法是否构成重写:

public void eat1(){
        System.out.println(this.name + " 正在吃学生餐");
    }

如上图:我们父类中并没有 eat1() 这个方法,构成不了重写,所以就报了这个错误

重写的权限

只能重写继承而来的方法(1)

因为重写是在子类重新实现从父类继承过来的方法时发生的,所以只能重写继承过来的方法。这意味着,只能重写那些被 public、protected 或者 default 修饰的方法,private 或者修饰的方法无法被重写

 错误示范:

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

class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }
    @Override
    public void eat(){
        System.out.println(this.name+" 吃狗粮");
    }
}

public class Test{
    public static void main(String[] args) {
        Animal a = new Dog("大黄",10);
        a.eat();
    }
}

 final、static 不能被重写(2)

<1> 因为 final 修饰的方法都是【封闭】方法不能被重写

<2> 重写的目的在于父类引用可以根据子类对象的运行时实际类型不同而调用不同实现代码,从而表现出多态。但是 static 静态方法不需要借助引用就可以调用

错误示范:

class Animal {
    public static String name;
    public int age;
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
    static void interest(){
        System.out.println("在睡觉");
    }

    final void eat(){
        System.out.println("正在吃");
    }
    
}

class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }
    @Override
    public void eat(){
        System.out.println(this.name+" 吃狗粮");
    }
    @Override
    static void interest(){
        System.out.println("狗睡觉");
    }
    
}

public class Test{
    public static void main(String[] args) {
        Animal a = new Dog("大黄",10);
        a.eat();
    }
}

重写的方法不能带有等级更严格的修饰符(3) 

如果子类被重写,那么子类的访问修饰符一定要大于等于父类的权限

修饰符权限大小排序:

private < 包访问权限 < protected < public

构造方法不能被重写(4)

因为构造方法很特殊,而且子类的构造方法不能和父类的构造方法同名(类名不同),所以构造方法和重写之间没有任何关系

在子类中通过 super 可以调用父类的重写(5)

class Person {
    protected String name;
    protected int age;
    protected int id;
    public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }
    public void eat(){
        System.out.println("正在吃");
    }
    public void interest(){
        System.out.println("玩游戏");
    }
}

class Student extends Person {
    public String interest;
    public void interest(){
        super.eat();
        System.out.println(this.name+" 玩游戏");
    }
    public Student(String name, int age, int id) {
        super(name, age, id);
    }
    @Override
    public void eat(){
        System.out.println(this.name + " 正在吃学生餐");
    }
}


class Test {
    public static void eat(Person a){
        a.eat();
    }
    public static void main(String[] args) {
        Person s1 = new Student("王五",18,2024234);
        s1.eat();
        System.out.println("---------------------------------------------");
        s1.interest();
    }
}

重载与重写

重载:

重载只存在于继承,是子类与父类之间的一种多态体现,重载的方法名、参数列表、返回值必须相同

重写:

重写存在于类中,是一个类的多态体现,重写的方法名必须相同,参数列表、返回值至少有一个不同

区别点重写(override)重载(override)
参数列表一定不能修改必须修改
返回类型一定不能修改【除非可以构成父子类关系】可以修改
访问限定符一定不能做更严格的限制(可以降低限制)可以修改

静态绑定与动态绑定

在 Java 中,当你调用一个方法时,可能会在编译时期解析,也可能实在运行时期解析,这全取决于到底是一个静态方法还是一个虚方法。如果是在编译时期解析,那么就称之为静态绑定,如果方法的调用是在运行时期解析,那就是动态绑定

Java 是一门面向对象的编程语言,优势就在于支持多态。多态使得父类型的引用变量可以引用子类型的对象

动态绑定的概念:

如果调用子类型对象的一个虚方法,编译器将无法找到真正需要调用的方法,因为它可能是定义在父类型中的方法,也可能是在子类型中被重写的方法,这种情形,只能在运行时进行解析,因为只有在运行时期,才能明确具体的对象到底是什么。这也是我们俗称的运行时或动态绑定

静态绑定的概念:

另一方面,private static 和 final 方法将在编译时解析,因为编译器知道它们不能被重写,所有可能的方法都被定义在了一个类中,这些方法只能通过此类的引用变量进行调用。这叫做静态绑定或编译时绑定。所有的 private static 和 final 方法都通过静态绑定进行解析

动态绑定与静态绑定这两个概念的关系,与 “方法重载”(静态绑定)和 “方法重写”(动态绑定)类似。动态绑定只有在重写可能存在时才会用到,而重载的方法在编译时期即可确定:
 

总而言之,其区别如下:

静态绑定在编译时期,动态绑定在运行时期
静态绑定只用到类型信息,方法的解析根据引用变量的类型决定,而动态绑定则根据实际引用的的对象决定
在 java 中,private static 和 final 方法都是静态绑定,只有虚方法才是动态绑定

多态是通过动态绑定实现的

多态的运用

多态在实际生活中的运用

我拿手机迭代的方式举个例子:

若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅 可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了


这是不是跟我们的重写很相像:重新定义一个新的类,来重复利用其中共性的内容, 并且添加或者改动新的内容

多态在代码练习中的运用

能够降低代码的重复性

这里用代码来举个例子:
在没有学多态以前,我们碰到一个需要判断在执行调用那个方法的操作往往是通过 if-else 来实现的。如图所示:

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

class Rect extends Shape {
    @Override
    public void draw() {
        System.out.println("画一个矩形!");
    }
}
class Cycle extends Shape{
    @Override
    public void draw() {
        System.out.println("画一个圆圈!");
    }
}

class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("画一个三角形!");
    }
}

class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("画一朵花!");
    }
}

public class Test2 {
    public static void drawMap(Shape shape) {
        shape.draw();
    }

    public static void drawMaps1() {
        Rect rect = new Rect();
        Cycle cycle = new Cycle();
        Triangle triangle = new Triangle();

        String[] shapes = {"1", "2", "2", "1", "3"};

        for(String s : shapes) {
            if(s.equals("1")) {
                cycle.draw();
            }else if(s.equals("2")) {
                rect.draw();
            }else if(s.equals("3")) {
                triangle.draw();
            }
        }
    }

    public static void main(String[] args) {
        drawMaps1();
    }

}

这样写的话你会发现十分的繁琐,现在你学会了多态你变可以这么写:

public class Test2 {
    public static void drawMap(Shape shape) {
        shape.draw();
    }

    public static void drawMaps() {
        Rect rect = new Rect();
        Shape shapeCycle = new Cycle();
        Triangle triangle = new Triangle();
        Flower flower = new Flower();

        Shape[] shapes = {shapeCycle,rect,rect,
                shapeCycle,triangle,flower};
        for(Shape shape : shapes) {
            shape.draw();
        }
    }
    public static void main(String[] args) {
        drawMaps();
    }

}

还可以这么写:

    public static void main1(String[] args) {
        drawMap(new Cycle());
        drawMap(new Rect());
        drawMap(new Triangle());
    }

这里是多态的魅力,我们不需要去判断 shape 去调用那个子类,只需引用父类。在程序运行时系统会动态绑定、向上转型,从而我们父类会获得子类中与自身匹配的方法

 

可扩展能力更强

如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低

class rhombus extends Shape {
    @Override
    public void draw() {
        System.out.println("画一个菱形!");
    }
}

对于类的调用者来说(drawShapes方法),只要创建一个新类的实例就可以了,改动成本很低。而对于不用多态的情况,就要把 drawShapes 中的 if - else 进行一定的修改,改动成本更高

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

烟雨长虹,孤鹜齐飞

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

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

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

打赏作者

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

抵扣说明:

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

余额充值