继承
Java 继承的实现(只支持类的单继承,不支持类多继承,但是支持接口的多实现)。
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类. 定义类时直接通过 extends 关键字指明要继承的父类。
子类对象除了可以访问子类中直接定义的成员外,可直接访问父类的所有非私有成员。
继承的作用
- 继承提高了代码的复用性。
- 继承的出现让类与类之间产生了关系,提供了多态的前提。
- 不要仅为了获取其他类中某个功能而去强行使用继承,类与类之间要有所属( " is a " )关系)。
如何使用一个继承体系中的功能
- 查阅父类功能(定义了共性的功能)
- 创建子类对象使用功能(因为父类可能不能创建对象, 而且子类提供了更丰富的功能)
- 继承中自子类变量的特点:如果子类出现非私有的同名变量时, 子类访问本类变量用 this, 子类访问父类中的同名变量用 super。
成员变量的隐藏
子类成员变量与父类一样,会屏蔽父类中的成员变量,称为“成员变量隐藏”。
方法的覆盖(Override)
如果子类方法完全与父类方法相同,即:相同的方法名、相同的参数列表和相同的返回值,只是方法体不同,这称为子类覆盖(Override)父类方法。
在声明方法时最后添加@Override
注解,@Override
注解不是方法覆盖必须的,但添加 @Override 注解有两个好处:
- 提高程序的可读性。
- 编译器检查 @Override 注解的方法在父类中已定义的方法是否匹配,如果不匹配则会报错。
方法覆盖时应遵循的原则
- 覆盖后的方法不能比原方法有更严格的访问控制(可以相同)。例如将代码访问控制 public 修改 private,那么会发生编译错误。
- 覆盖后的方法不能比原方法产生更多的异常。
- 父类中的私有方法不可以被覆盖。
- 子类的返回类型可以是父类的子类。
覆盖的应用
- 当子类需要父类的功能,而功能主体子类有自己特有内容时,可以复写父类中的方法,这样也沿袭了父类的功能
- 构造方法在类继承中的作用构造方法不能继承.由于子类对象要对来自父类的成员进行初始化,因此,在创建子类对象时除了执行子类的构造方法外,还需要调用父类的构造方法.具体遵循如下原则:
- 当子类未定义构造方法时,创建对象时将无条件地调用父类的空构造方法,会默认在第一条添加super();
- 对于父类的含参数构造方法,子类可以在自己构造方法中使用关键字 super 来调用它, 但 super 调用语句必须是子类构造方法中的第一个可执行语句;
- 子类在自己定义构造方法中如果没有用 super 明确调用父类的构造方法,则在创建对象时,将自动先执行父类的无参构造方法,然后再执行自己定义的构造方法。
所以在一个类的设计时如果有构造方法,最好提供一个无参构造方法。例如系统类库中的类大多提供了无参构造方法,用户编程时最好也要养成此习惯。
【注意】使用 this 查找匹配的方法时首先在本类查找,找不到时再到其父类和祖先类查找;使用 super 查找匹配方法时,首先到直接父类查找,如果不存在,则继续到其祖先类逐级往高层查找。
继承的更多细节
❑ 构造方法;
在父类构造方法中调用可被子类重写的方法,是一种不好的实践,容易引起混淆,应该只调用private的方法。
❑ 重名与静态绑定;
静态绑定在程序编译阶段即可决定。实例变量、静态变量、静态方法、private 方法,都是静态绑定的。
而动态绑定则要等到程序运行时。子类可以重写父类非private的方法,当调用的时候,会动态绑定,执行子类的方法。
❑ 重载和重写;
重载是指方法名称相同但参数签名不同(参数个数、类型或顺序不同),重写是指子类重写与父类相同参数签名的方法。
对一个函数调用而言,可能有多个匹配的方法,有时候选择哪一个并不是那么明显。当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先是按照参数类型进行匹配的,换句话说,寻找在所有重载版本中最匹配的,然后才看变量的动态类型,进行动态绑定。
❑ 父子类型转换;
类型转换有两个方向:
- 将父类引用类型变量转换为子类类型,这种转换称为向下转型(downcast);
- 将子类引用类型变量转换为父类类型,这种转换称为向上转型(upcast)。向下转型需要强制转换,而向上转型是自动的。
将父类引用赋值给子类变量时要进行强制转换,强制转换在编译时总是认可的,但运行时的情况取决于对象的值.如果父类对象引用指向的就是该子类的一个对象,则转换是成功的。否则会抛出ClassCastException
。如果不能确定实例是哪一种类型,可以在转型之前使用 instanceof
运算符进行判断。
因此并不是所有的引用类型都能互相转换,只有属于同一棵继承层次树中的引用类型才可以转换。
❑ 可见性重写;
重写方法时,一般并不会修改方法的可见性。但我们还是要说明一点,重写时,子类方法不能降低父类方法的可见性。
为什么要这样规定呢?继承反映的是“is-a”的关系,即子类对象也属于父类,子类必须支持父类所有对外的行为,将可见性降低就会减少子类对外的行为,从而破坏“is-a”的关系,但子类可以增加父类的行为,所以提升可见性是没有问题的。
举例:
// 父类 Object lalala(Integer x) throws IllegalArgumentException { // 子类 @Override Integer lalala(Integer x);
子类只能抛出范围内或者更小的异常,返回类型也只能更小,但是方法的可见性可以更大。虽然重写方法时,一般并不会修改方法的签名。
❑ 防止继承(final)
final关键字可以修饰变量,也可修饰final 使成为最终类。
继承是把双刃剑
继承破坏封装什么是封装呢?封装就是隐藏实现细节,提供简化接口。使用者只需要关注怎么用,而不需要关注内部是怎么实现的。实现细节可以随时修改,而不影响使用者。
继承可能破坏封装是因为子类和父类之间可能存在着实现细节的依赖。子类在继承父类的时候,往往不得不关注父类的实现细节,而父类在修改其内部实现的时候,如果不考虑子类,也往往会影响到子类。
继承既强大又有破坏性,那怎么办呢?1)避免使用继承;2)正确使用继承。
怎么避免继承的有三种方法:
❑ 使用final关键字;
❑ 优先使用组合而非继承;
❑ 使用接口。
使用组合,子类就不需要关注基类是如何实现的了,基类修改实现细节,增加公开方法,也不会影响到子类了。
多态性
父类或者接口的引用指向或者接收自己的子类对象叫多态 。实际执行调用的是子类实现,这叫动态绑定。
作用:多态的存在提高了程序的扩展性和后期可维护性.
发生多态要有三个前提条件:
- 继承。多态发生一定要子类和父类之间。
- 覆盖。子类覆盖了父类的方法。
- 声明的变量类型是父类类型,但实例则指向子类实例。
UML图简介
UML是 Unified Modeling Language 的缩写,即统一标准建模语言。它集成了各种优秀的建模方法学发展而来的。UML 图常用的有例图、协作图、活动图、序列图、部署图、构件图、类图、状态图。
面向对象分析与设计(OOAD)时,会用到 UML 图,其中类图非常重要,用来描述系统静态结构。Student 继承 Person的类图如图所示。类图中的各个元素说明如图所示,类用矩形表示,一般分为上、中、下三个部分,上部分是类名,中部分是成员变量,下部分是成员方法。实线+空心箭头表示继承关系,箭头指向父类,箭头末端是子类。UML 类图中还有很多关系,如图所示,如图虚线+空心箭头表示实线关系,箭头指向接口,箭头末端是实线类。
类图中的元素
参考
- 丁振凡编著,《Java 语言程序设计(第2版)》华东交大版,2014.9