本篇博客主要介绍类的继承和组合相关概念。
继承
代码中创建出来的类,主要是为了抽象现实中的一些事物(包含属性和方法)。
有的时候客观事物之间就存在一些关联关系,那么在表示成类和对象的时候也会存在一定的关联。
下面我们来看一个例子:
- 我们来创建三个类。
我们可以发现,这个代码中存在大量冗余的代码。Animal和Rabbit以及Fish这几个类中存在一定关系。
这三个类都具有一个相同的eat方法,而且行为是完全一样的,都具有一个相同的name属性,而且意义是一样的。从逻辑上来讲,Rabbit和Fish都是一种Animal(is-a语义)。
我们可以让Rabbit和Fish分别继承Animal类,来达到代码重用的效果。此时,Animal这样被继承的类,我们称为父类,基类或超类,对于像Rabbit和Fish这样的类,我们成为子类,派生类。
和现实中儿子继承父亲财产类似,子类也会继承父类的字段和方法,以达到代码重用的效果。
语法规则
基本语法:
class 子类 extends 父类 {}
- 使用extends指定父类;
- Java中一个子类只能继承一个父类(C++/Python中支持多继承);
- 子类会继承父类的所有public字段和方法,对于父类的private的字段和方法,也会继承,但是子类中无法访问;
- 子类的实例中,也包含着父类的实例。可以使用super关键字得到父类实例的引用。
对于前面的代码,我们使用继承进行改进。此时我们让Rabbit和Fish继承自Animal类,那么Rabbit和Fish在定义的时候就不必再写name字段和eat方法。
- 代码如下:
extends英文原意为“扩展”。而我们所写的类的继承,也可以理解成基于父类进行代码上的“扩展”。例如我们写的Fish类,就是在Animal类的基础上扩展了swim();
方法。 - 此时,我们将父类的name字段改为private。
protected关键字
我们发现,如果把字段设为private,子类不能访问。但是设成public,又违背了我们“封装”的初衷。
两全其美的方法就是protected关键字。
- 对于类的子类和同一个包的其他类来说,protected修饰的字段和关键字是可以访问的。
- 对于类的调用者来说,protected修饰的字段和方法是不能访问的。
四种访问权限
Java中对于字段和方法共有四种访问权限。
- private:类内部能访问,类外部不能访问;
- 默认(也叫包访问权限):类内部能访问,同一个包中的类可以访问,其他类不能访问;
- protected:类内部能访问,子类和同一个包中的类可以访问,其他类不能访问;
- public:类内部和类的调用者都可以访问。
No | 范围 | private | default | protected | public |
---|---|---|---|---|---|
1 | 同一个包中的同一个类 | √ | √ | √ | √ |
2 | 同一包中的不同类 | √ | √ | √ | |
3 | 不同包中的子类 | √ | √ | ||
4 | 不同包中的非子类 | √ |
什么时候用哪一种?
- 我们希望类要尽量做到“封装”,即隐藏内部实现细节,只暴露出必要的信息给类的调用者;
- 因此我们在使用的时候应该尽可能的使用比较严格的访问权限。例如如果一个方法能用private,就尽量不要用public;
- 另外,还有一种简单粗暴的做法,将所有的字段设为private,将所有的方法设为public。不过这种方法属于是对访问权限的滥用,不推荐使用。
更复杂的继承关系
这个时候使用继承方式来表示,就会涉及到更复杂的体系。
- 我们来写一下草鱼的继承过程:
- 上面这样的继承方式成为多层继承,即子类还可以进一步的再派生出新的子类。
- 注意:一般我们不希望出现超过三层的继承关系。如果继承层次太多,就需要考虑对代码进行重构了。
- 如果想要从语法上限制继承,可以使用final关键字。
final关键字
- 我们知道final关键字,修饰一个变量或者字段的时候,表示常量(不能修改)。
- final关键字也能修饰类,表示类不能被继承。
组合
和继承类似,组合也是一种表达类之间关系的方式,也是能够达到代码重用的效果。
- 例如表示一个学校。
组合没有涉及到特殊的语法,仅仅是将一个类的实例作为另外一个类的字段。
继承和组合分别什么时机使用
- 组合表示has-a语义。如上中,学校有老师和学生,符合has-a语义,所以使用组合。
- 继承表示is-a语义。前面的例子中,鱼是动物,符合is-a语义,所以使用继承。
- 使用组合还是继承,要具体看实际场景符合哪种语义。
- 优先考虑使用组合。
为什么优先考虑组合
- 我们知道,使用继承实现新的类非常容易,因为基类的大部分功能都可以通过继承关系自动赋予派生类;修改或者扩展继承来的非常容易。
- 初学面向对象编程的人会认为继承真是一个好东西,是实现复用的最好手段。但是随着应用的深入就会发现继承有很多缺点:继承破坏封装性。基类的很多内部细节都是对派生类可见的,因此这种复用就是“白箱复用”;如果基类的实现发生改变,那么派生类的实现也将随着改变。这样就导致了子类行为的不可预知性;从基类继承来的实现是无法在运行期键动态改变的,因此降低了应用的灵活性。
- 而合理的使用组合可以有效的避免继承的缺点,使用组合关系将系统对变化的适应能力从静态提升到动态,而且由于组合将已有对象组合到了新对象中,因此新对象可以调用已有对象的功能。由于组合关系中各个对象的内部实现是隐藏的,我们只能通过接口调用,因此我们完全可以在运行期实现了同样接口的另一个对象来代替原对象,从而灵活实现运行期的行为控制。而且使用合成关系有助于保持每个类的职责的单一性,这样类的层次体系以及类的规模都不太可能增长为不可控制的庞然大物。因此我们优先使用组合而不是继承。
- 当然这并不是说继承是不好的,我们可用的类总是不够丰富,而使用继承复用来创建一些实用的类将会比组合来的更快,因此在系统中合理的搭配使用继承和组合将会使你的系统强大而有牢固。