08-多态
1,什么是多态?
多态是继数据抽象和继承之后的第三种基本特征。多态通过封装将接口(这里接口并非单指interface,而是包括抽象类,接口和普通父类)和实现分离开。多态能改善代码的组织结构和可读性。上篇笔记中写了“向上转型”,导出类(子类)可以被认为是基类(父类)。多态用它消除类型之间的耦合关系,代码中可以避免限定某个具体类型实例,而是以父类类型接收不同子类实例类型传入并调用。
2,向上转型和多态
回顾一下向上转型,创建几何类,和四边形类,以及圆形类,四边形和圆形类都继承几何类,几何类拥有获取周长的方法,子类覆盖并实现。示例如下:
public class GeometricFraph { public static void main(String[] args) { perimeter(new Square()); perimeter(new Circular()); } /** * 多态的形式调用方法 * @param geometric */ public static void perimeter(Geometric geometric){ //根据实际的实例调用对应的方法 geometric.perimeter(); } public static void perimeter(Square square){ System.out.println("Square"); //根据实际的实例调用对应的方法 square.perimeter(); } } /** * 几何类 */ class Geometric{ /** * 获得周长 */ public void perimeter(){ System.out.println("外围总长"); } } /** * 四方形 */ class Square extends Geometric{ /** * 四边形获得周长的方式 */ @Override public void perimeter(){ System.out.println("四边总长"); } /** * 四方形的宽度 * 扩展方法 */ public void width(){ System.out.println("宽度"); } } /** * 圆形 */ class Circular extends Geometric{ /** * 圆形获得周长的方式 */ @Override public void perimeter(){ System.out.println("圆圈总长"); } /** * 圆形的直径 * 扩展方法 */ public void diameter(){ System.out.println("直径"); } }
结果:
Square
四边总长
Geometric
圆圈总长
如上两个perimeter方法参数分别是Geometric基类和Square导出类(子类),调用perimeter方法时如果传入的是Square实例,调则用的是perimeter(Square square)方法。实际上如果没有perimeter(Square square) 方,则默认会进入perimeter(Geometric geometric)方法,就比如传入的new Circular()调用的是perimeter(Geometric geometric)方法,而且geometric参数准确调用了Circular的perimeter方法。
多态的好处已经看到了吧,正常情况下,我们只需要写一个perimeter(Geometric geometric)方法就可以了,它可以接收它的所有导出类,并能准确的执行导出类正确的方法,试想下如果没有多态时什么样子的,应该每个子类都要写个方法,每新增一个新的几何类,都要对应生成一个新的方法。对代码的维护和组织结构都是很大的破坏。
3,方法调用绑定
正如上面示例,虽然传入的是Circular类型的实例,而父类Geometric参数正确调用其子类Circular的方法,其中原理涉及方法绑定。
3.1 前期绑定
多态是针对从基类继承并在子类覆盖的方法,如果某些方法注定不会被覆盖,例如final修饰和static修饰的方法,private方法也是隐式的final方法,那这些方法就是前期绑定方法。程序编译期明确知道会被哪个类调用,所以这种在程序执行前绑定好的方法,就叫做前期绑定。又叫做静态绑定。而除了这些方法在java中所有方法都是后期绑定的, 后期绑定就是运行期绑定,在运行时根据具体对象的类型进行绑定。只有运行时才知道方法会被哪个类调用。
3.2 动态绑定
大家都知道方法名+方法参数表组成方法签名,是调用方法识别方法的依据。而java如何实现在运行时把导出类识别到其基类参上呢?答案是RTTI,RTTI是运行时类型检查,java的每个.java文件编译运行后会在内存保存class信息,class信息里面包含方法,域等等信息,通过类型信息可以实现反射,动态加载等等功能,后面会有笔记写到。这里单指多态动态绑定方面。如下示例通过类信息判断实际的实例对象:
public static void main(String[] args) { getClassInfo(new Circular()); } public static void getClassInfo(Geometric geometric) { Class clazz = geometric.getClass(); System.out.println("Geometric参数实际的类" + clazz.getName()); System.out.println(Arrays.deepToString(clazz.getDeclaredMethods())); } 结果: Geometric参数实际的类:com.javaApi.override.Circular [public void com.javaApi.override.Circular.perimeter(), public void com.javaApi.override.Circular.diameter()]
从上打印的结果可以看出java运行时通过参数获取其class信息并识别具体实例,以及拥有的可调用的方法集合信息。RTTI通过类型完成动态绑定工作,关于RTTI和类信息以后会有笔记详解讲。
4,缺陷:"覆盖"私有方法(private方法)
上面提到多态是针对从基类继承并在子类覆盖的方法,private修饰的方法不能被继承,所以它不适用多态,虽然它是隐式的final但它不像final限制子类不能重写,也就是说private修饰的方法,子类也可以再写一份一样的方法,只不过它们之间是没有任何关系。
实例如下:
/** * 几何类 */ class Geometric { /** * 对角线 */ private void diagonal(){ System.out.println("几何总长"); } } /** * 圆形 */ class Circular extends Geometric { /** * 对角线 */ public void diagonal(){ System.out.println("几何总长"); } }
注意,父类和子类都有diagonal方法,但它们俩没有任何关系,而且此方法也不支持多态,如果有访问权限(private只能本类调用访问权限),Geometric参数只会调用自身的diagonal方法。
4,缺陷:域和静态方法
java的多态只是针对方法,而不是适用于域。原因是因为父类的域和子类的域分配不同的存储空间,虽然域可能是一模一样的,但它们是相互隔离的,子类默认调用自身的域,而调用父类域的话通过super.域。示例如下:
class Geometric { public String name="几何"; public void getName(){ System.out.println("name:"+name); } } class Circular extends Geometric { public String name="圆形"; @Override public void getName(){ System.out.println("name:"+name+",super.name:"+super.name); } } public static void main(String[] args) { getName(new Circular()); } public static void getName(Geometric geometric) { geometric.getName(); } 结果: name:圆形,super.name:几何
4.1静态方法不支持多态
多态是针对从基类继承并在子类覆盖的方法,所以静态方法不支持多态的。构造器也不适用于多态,因为构造器也是隐式的static方法。
5,构造方法和多态
构造方法是隐式static方法,所以它本身不是多态方法,但是它能够影响到多态,如果不加谨慎的话可能会导致程序发生问题。构造器的调用流程在之前的笔记中已经写过,简单回顾下,在没有静态域和静态块的前提下,构造器调用会按照继承层次逐渐向上链接,每个基类的构造器都能被调用,构造器调用过程会检查对象是否被正确地构造。基类会先进行域的初始化再调用构造方法,然后子类进行域的初始化,再调用子类的构造方法。但是子类和父类的域都会先设定默认值。
示例如下:
class Geometric { public String name="几何"; public void getName(){ System.out.println("name:"+name); } /** * 父类构造器 */ public Geometric(){ //调用getName()方法,且getName方法支持多态 getName(); } } class Circular extends Geometric { public String name="圆形"; @Override public void getName(){ System.out.println("name:"+name+",super.name:"+super.name); } } public static void main(String[] args) { getName(new Circular()); } public static void getName(Geometric geometric) { geometric.getName(); } 结果: name:null,super.name:几何 name:圆形,super.name:几何
通过结果分析,为什么第一次name为null,第二次又有值了呢?就上像提到的构造器调用顺序,new Circular()时会先调用父类的构造器并在父类构造器中调用了getName方法,而此时子类name值还没有初始化所以其默认值是null,而getName方法支持多态,所以在父类构造器中调用的是子类的getName方法,所以这就是结果name为null的原因。
而第二次name有值是因为父类构造器执行完后,子类开始初始化域先初始化,name被赋值为"圆形",再调用getName方法时,name已经初始化后的值。
6,协变返回类型
协变返回类型表示导出类中方法可以返回基类类型。示例如下:
public static Geometric getGeometric() { return new Circular(); }
7,向下转型
向下转型及父类引用转型成子类,通过强制性转换,但转换前最好先进行类型判断,否则强转后可能会抛ClassCastException异常,示例如下:
//类型判断,co是否是Circular类型 if (co instanceof Circular){
Circular cc= (Circular)co;//向下转型 cc.getName();
}
8 模板方法设计模式
多态在java程序开发中是非常重要的功能,程序中大量的使用多态的方式。我觉得要说最能体现多态的用法,模板方法设计模式最能体现。模板方法设计模式用于先将代码的逻辑算法结构搭建起来,将公共的方法提供给子类覆盖实现,而具体实现由子类完成,详情可以看去网上下模板方法设计模式,我后面也会写java设计模式笔记。
9 总结
为了让程序有效的运行多态乃至面向对象的技术,必须扩展自己的编程视野,尽量在少量的代码中实现更多的功能,可以减少后期维护以及大量维护带来的风险,以及更快的程序开发,更好的代码组织,更好的扩展程序等。