Java面向对象之——多态

一、多态的概念

多态是同一个行为具有多个不同表现形式或形态的能力。就比如人吃饭,对于中国人使用筷子吃饭,美国人使用刀叉,印度人用手,不同的对象对同一个方法的调用表现出的行为是不一样的。

假如现在有一个Shap的基类,同时有一个drow()方法用来画图形,Shap是一个抽象的类,又可以派生出一些子类如Circle-画圆、Triangle-画三角形、Square-画正方形。如何将Shap-画图这件事情,发生在不同对象身上,产生不同的结果,这就是我们多态要研究的内容。

二、多态的条件

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

  1. 必须在继承体系下
  2. 子类必须要对父类中方法进行重写
  3. 向上转型:父类引用指向子类对象Parent p = new Child();
  4. 通过父类的引用调用重写的方法

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

上面的4种条件中,继承和方法的调用我们已经很熟悉了,下面就围绕,重写和转型进行详细介绍:

三、重写

重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

方法重写的规则

  1. 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致 被重写的方法返回值类型可以不同,但是必须是具有父子关系的。
  2. 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected.
  3. 父类被static、private、final修饰的方法、构造方法都不能被重写。
  4. 重写的方法, 可以使用 @Override 注解来显式指定。有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写。

重写的设计原则

对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动(重写)新的内容。比如新旧手机:

重写和重载的区别

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

通常认为:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

其实重写的返回值类型一般是不做修改的,但是也有可修改的情况,这里我查阅了一下,大家可以看一下这个例子:

    class Test1 { 
       public Object workO() { 
           return new Object(); 
       } 
   } 
   class Test2 extends Test1 {
       @Override 
       public String workO() { 
           return new String(); 
       } 
   } //其中String是Object的子类型.

☆特别注意☆避免在构造方法中调用重写的方法

class B {
    public B() {
        func();
    }
    public void func() {
        System.out.println("B.func()");
    }
}
class D extends B {
    private int num = 1;
    @Override
    public void func() {
        System.out.println("D.func() " + num);
    }
}
public class Test {
    public static void main(String[] args) {
        D d = new D();
    }
}

分析: 我们已知程序执行顺序为:先执行父类静态代码块–>执行子类静态代码块–>执行父类实例代码块–>执行父类构造方法–>执行子类实例代码块–>执行子类构造方法。实例化对象d时会调用子类d的构造方法,在子类构造方法中需要先对基类B的构造方法进行构造,此时会调用方法func()由于func()为重写方法,此时发生动态绑定又回去调用子类D中重写的func()方法,但是此时的num还未赋值默认值为0,所以输出结果为:D.func() 0

结论: 在实际开发中"用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。

四、向上转型和向下转型

1、向上转型

向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。

基本语法格式父类类型 对象名 = new 子类类型()

向上转型的几种应用场景:

1. 直接赋值

shap是父类类型,但可以引用一个子类对象,此时是从子类向父类转换,从小范围向大范围的转换。

Shap shap=new Circle();

2. 父类数组存放子类对象

父类数组可以放子类类型的对象-自动发生向上转型

Shape[] shapes = {cycle,rect,cycle,rect,flower};

3. 方法传参

形参为父类型引用,可以接收任意子类的对象

public static void drowshap(Shap a){
	a.drow();
}

4. 方法返回

父类接收返回任意子类对象

public static Shap shap() {
	return new Circle;
}

注意:向上转型后不能调用子类特有的方法。

2、向下转型

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

//向上转型
Shap shap = new Circle();
//向下转型,本来指向的就是圆,因此将shap还原为圆也是安全的
Circle cir = (Circle)shap;//非父子关系,需要强制类型转换
//此时可以访问子类的方法
cir.drow();
//向上转型
Shap shap = new Circle();
//向下转型
//shap实际指向的是圆,现在要强制还原为三角,
//无法正常还原,运行时抛出:ClassCastException
Triangle tri = (Triangle)shap;//error

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

instanceof关键字:的作用是判断左边对象是否是右边类的实例,返回的boolean类型,truefalse.

所以我们可以在代码中加入这些代码保障向下转型的安全:

if(shap instanceof Circle){
	Circle cir = (Circle)shap;
	cir.drow();
}
if(shap instanceof Triangle){
	Triangle tri = (Triangle)shap;
	tri.drow();
}

五、再谈多态

前面我们一直在谈论多态,对于多态,是同一个行为具有多个不同表现形式或形态的能力,而这种能力实现的本质其实是动态绑定,谈到这我们有必要谈谈起动态绑定和静态绑定:

动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。

静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载

一顿操作下来,实现多态的条件我们以经介绍完毕,对于多态的本质也有了一定的了解。下面我们以本文开头的Shap类简单的实现一下多态:

class Shape {

    public void draw() {
        System.out.println("画图形!");
    }
}
class Square extends Shape {
    @Override
    public void draw() {
        System.out.println("□!");
    }
}
class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("○!");
    }
}
class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("△!");
    }
}

 public static void drawMap() {
        Square rect = new Square();
        Circle cycle = new Circle();
        Flower triangle = new Triangle();
		//父类数组可以放子类类型的对象-自动发生向上转型
        Shape[] shapes = {circle,square,triangle};
		//调用重写方法
        for(Shape shape : shapes) {
            shape.draw();
        }
    }

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

六、多态的优缺点

介绍完多态后,我们简单做个总结,盘点一下多态的优缺点。

多态优点:

1.能够降低代码的 “圈复杂度”, 避免使用大量的 if - else(圆圈复杂度:一种代码复杂度的衡量标准)

例如将如上多态写出循环,其中就会出现大量的if-else

    public static void drawMap2() {
        Square square = new Square();
        Circle circle = new Circle();
        Triangle triangle = new Triangle();

        String[] shapes = {"circle", "square", "triangle"};
        for (String s : shapes) {
            if(s.equals("circle")) {
                circle.draw();
            }else if(s.equals("square")) {
                square.draw();
            }else {
                triangle.draw();
            }
        }
    }

2.可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低。对于类的调用者来说, 只要创建一个新类的实例就可以了, 改动成本很低。而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高。

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

多态缺点代码运行效率降低

总结

本章主要探讨多态的使用,重点介绍了转型和重写的概念,至此面向对象的三大特性就全部讲完了,这部分内容比较抽象,希望大家多总结,多思考,我们一起快乐编程!🥰

评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不摸鱼的程序员

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

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

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

打赏作者

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

抵扣说明:

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

余额充值