JAVA学习之多态

简介

在面向对象语言中,多态是继数据抽象和继承之后的第三种基本特征。
多态通过分离抽象和实现,改善代码的组织结构和可读性,并能够创建可扩展的程序。
封装通过合并特征和行为来创建新的数据类型。而多态则是消除了类型之间的耦合关系,允许将多种类型视为统一类型来处理,也就是说我们使用多态时可以面向抽象进行编程而不用在乎底层实现。通过一套代码就可以实现不同的业务逻辑

向上转型

在前面的复用类一章中(https://blog.csdn.net/qq_33905217/article/details/109448142)我们谈到了向上转型,对象既可以作为它自己本身的类型使用,也可以作为它的基类的类型使用。我们把这种对某个对象的引用视为对基类类型的引用的做法称作向上转型。

通过向上转型我们可以让不同的子类实现作用于同一段代码,从而产生不同的逻辑。但是这样存在一个问题,当发生了向上转型之后编译器无法获知目标对象的真正类型,当然,写的人肯定是知道的。(如果有人说,我可以通过class对象获取到目标的实际类型,是的,这样是可以获取到,但是注意,这种获取类型的方式发生在运行时期,也就是说,只要程序不运行,你就无法获知传入的到底是哪个类型)

转机

方法调用绑定

将方法调用同一个方法主体关联起来被称作绑定,若程序在执行前进行绑定叫做前期绑定。这是面向过程语言的默认绑定方式。如果我们的java实现的是前期绑定,也就是说程序需要在编译时就需要确定要被调用的方式,这听起来好像不难,但是举个例子

class A{
	public void f(){
		System.out.println("AAAAA");
	}
}

class B extends A{
	public void f(){
		System.out.println("BBBBB");
	}
}

class C{
	public void play(A a){
		a.f();
	}
}

在上面代码中,B继承了A同时重写了f()方法,也就是说,现在A的f()和B的f()是不同的两个方法,但是此时C的play方法接收一个A类并调用a的f()方法,但是编译器只能知道会传入的是一个A类型,但是具体的类型它并不知道,所以它无法确定到底要调用哪个方法才对。(有人可能会问,那C语言为什么可以前期绑定,java就不行,明明编译器可以通过追溯的方法来获知到底是哪个类调用的方法嘛,事实上,除非编译器是从main方法开始,像运行程序一样,逐句进行编译才可能实现类型的获知。但是编译器很明显不可能这么做,这跟跑一边程序有什么区别,要是碰上循环了不得跑死。那如果不能这样去编译,那么编译器如何在编译到该方法时知道它要调用的是谁的f()方法)

解决方案就是将方法的绑定延后,延迟到运行时。这种绑定叫做运行时绑定或者叫后期绑定。如果一种语言想实现后期绑定,就必须具有某种机制,以便运行时能够判断对象的类型,从而调用恰当的方法。换句话说,编译器一直都不知道对象的类型,但是方法调用机制能够找到正确的方法体,并加以调用。

在java中除了static和final方法以外(private属于final方法),其他方法都是后期绑定。

当我们确定java是通过动态绑定来实现方法调用时,我们就可以没有顾及地编写只与基类打交道的代码了,因为我们知道不关怎么样,他都会为我们调用正确的对象方法。

可拓展性

多态为我们的程序带来了巨大的可扩展性,在以前面向过程的语言中,我们如果需求发生变化,基本上就是要修改原来的代码,这是违背面向对象的开闭原则的。所以多态为我们提供了一种拓展而非修改的方式来满足需求的变化。

举个例子,对于我们来说,我们需要汽车来进行代步,但是汽车是一个消耗品,没过几年我们都需要更换新的汽车,但是我们很明显不可能只够买同一款汽车。也就是说,我们驾驶的汽车是不断变化的。在这个例子中,不变的行为。不管我们购买了什么新款汽车。但是开车这一行为是不变的。所以我们这么设计

class Car{
	public void run(){
		System.out.println("汽车跑起来了");
	}
}

class Benz{
	public void run(){
		System.out.println("驾驶奔驰跑起来了");
	}
}

class GTR{
	public void run(){
		System.out.println("东瀛战神GTR跑起来了");
	}
}

class Driver{
	public void drive(Car car){
		car.run();
	}
}

在上面例子中,司机通过一个拿到一辆车来进行驾驶行为,但是司机也只是知道他要开的是辆车,具体开的是什么并不知晓,但是怎么开司机是知道的,所以当我们向给司机的是一辆奔驰,司机就会让奔驰跑起来,给他一辆GTR,司机就开始飙车,不关以后要开什么,只需要传入不同的汽车,而不需要修改司机开车的行为

缺陷:覆盖私有方法

在继承结构中,只有非private方法才可以被覆盖,所以当我们的导出类向上转型成基类时,基类中也存在一个属于基类的同名方法,也就是说,导出类随着转型,会执行对应引用类型的同名私有方法(非私有方法当然还是覆盖的)

public class A{
	private void f(){
		System.out.println("A.f()");
	}
	public static void main(){
		A a = new B();
		a.f();
	}
}

class B extends A{
	private void f(){
		System.out.println("B.f()");
	}
}

当执行了上面的代码后你会发现执行的是A中的f()。当然,由于你要执行A的f()是个私有方法,必须要在A的内部执行,所以一般开发过程中我们不必要担心这个问题。

缺陷:域和静态方法

在了解多态后,我们很自然的认为所有事物都可以多态地发生。但事实上,只有普通方法调用是可以多态的。涉及到域(成员变量)的访问。就会根据你的引用类型去访问对应的域

public static void main(String[] args) {
        A a = new B();
        System.out.println(a.i);
}

class A{
    public int i = 0;
}

class B extends A{
    public int i = 1;
}

运行以下上面的代码,我们会发现打印出来的是A中的i的值。如果在导出类中需要访问基类中的同名变量那就通过super关键字访问。
虽然看起来这很容易让人产生误会,但事实上我们在开发时,都会将成员变量设置成私有,并且我们也不太会去覆盖基类中的成员变量。如果某个方法是静态的,它的行为将不具有多态性。静态变量也是如此

构造器和多态

构造器并不具有多态性,实际上构造器是static方法,只不过是隐式地

基类地构造器总是在导出类地构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类地构造器都能得到调用。(导出类只能访问它自己地成员,不能访问基类中的成员(基类成员通常是private类型)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化,因此必须令所有构造器都得到调用,否则就不能正确构造完整对象。在导出类的构造方法中,如果没有显式调用父类构造器,编译器会自动写入一个父类的无参构造调用)

构造器内的多态方法的行为

我们来看一个有意思的例子,从这里例子中我们可以发现实例初始化的过程

public class demo1 {
    public static void main(String[] args) {
        new A2();
    }
}

class A1{
    public A1(){
        System.out.println("A1.constructor");
        f();
    }

    public void f(){}
}

class A2 extends A1{
    private int i =10;
    public A2(){
        super();
        System.out.println("A2.constructor");
        System.out.println("constructor:"+i);
    }

    @Override
    public void f(){
        System.out.println("f:"+i);
    }
}
/**Output
*
*
**/

解释一下:上面例子中,A2继承了A1,也就是说,在A2构造方法主体执行前会先执行A1的构造方法,而在A1的构造方法执行了一个f()方法,这个f()方法被导出类A2重写覆盖了,所以按照多态,此时A2中只有一个f()方法那就是A2中重写的f()方法。这意味着基类构造器中执行的f()方法就是重写的f()方法。这时候我们把眼睛放到f()中我们发现f()打印的结果是0;这表示在基类执行f()的时候i还没有完成初始化,这表示基类的构造方法执行在导出类的成员变量初始化之前。同时,我们发现在导出类中打印出的i值为10,这说明在导出类主体执行前,i完成了初始化。因此我们得出以下结论(实例的构造过程):

  1. 为实例分配的空间清零(所有成员变量初始化为0/null)
  2. 基类成员变量初始化
  3. 基类构造方法主体执行
  4. 导出类成员变量初始化
  5. 导出类构造方法主体执行

协变返回类型

JavaSE5中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型

class A{}

class B{}

class C{
	public A f(){}
}

class D{
	public B f(){}
}

用继承进行设计

学习了多态以后,看似所有的事物都可以被继承。但事实上,当我们创建新类时如果将继承作为首要选择,反而会加重我们的设计负担。比如继承关系中,基类对于导出类的侵入性过强,一旦基类发生变化,子类势必也需要发生变更,这很明显违反了面向对象编程中的开闭原则。
更好的方式是 首先选择组合,因为组合不会对我们的程序产生过强的侵入性,相反,它更加灵活,因为我们可以通过在新类中保留一个组合类型的引用对象,然后通过外界传入来动态选择器具体实现来选择行为。

继承虽然给我们带来巨大的好处,但是它的劣处也很明显,除了上面提到的强侵入性以外,由于我们经常通过上下转型来完成多态,这也使得那些实现了许多额外特性的导出类会在向上转型的过程中丢失类型,因为我们无法通过基类类型来知道传入的是什么类型,错误的向下转型只会为我们的程序带来巨大的隐患。当然这也不是不可以解决的,通过RTTI(运行时类型识别)我们也可以得到正确的类型从而实现正确的向下转型。关于RTTI我们这里只是提到以下,具体的我们专门出一章进行描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

原来是肖某人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值