继承
说起继承,我相信大家都不陌生。一个extends的关键字,就可以继承父类的public & protect方法与域属性。但是如果说继承要注意的事情我相信没多少人能答的出来。虽然这些非常细腻的知识对于平时小的开发并不影响,但是一但遇到大型开发,你就得考虑,继承是否随便就能用。
继承原理
对于继承的概念不需要解释很多,因为这类文章已经充斥着整个互联网。这里我想通过继承的原理分析,然后跟大家一起慢慢的去探究那些值得我们注意的坑。
请看下面的一段代码:
//Sample_1.java
import me.hades;
import static me.hades.Utils.*; // 这是个工具类,封装了系统的System.out.printlb(); --> println();
class Shape{
public Shape(){
println("I'm Shape Constructor");
}
}
class Circle extends Shape{
public Circle(){
println("I'm Circle Constructor")
}
}
public class Sample_1{
public static void main(String args[]){
new Circle();
}
}
仔细考虑一下这段代码运行的结果会是怎么样的?如果你是马上回答:打印I'm Circle Constructor
的话,那就要请你继续看下去了,因为你非常需要了解这些知识。
程序运行的结果应该是这样的:
I'm Shape Constructor
I'm Circle Constructor
为什么只new 了Circle类出来,怎么会打印出Shape的构造器里面的文字呢?
其实这就是继承的原理,因为Circle继承了基类Shape,当你在实例化Circle的时候,编译器会自动的帮你实例化一个内部隐藏的基类对象,还记不记得平时调用父类方法时候用的super.XXX()
没错,就是这个super对象,这是编译器在编译的时候帮我们实例化出来的。
继承是从最外的一层慢慢的扩散的。
因为Circle 继承了 Shape ,Circle的内部会实例化Shape的对象,既然是实例化那就会自然调用它的构造器方法,那怎么保证会调用到合适的构造器呢?
其实可以去尝试一下,现在的IDE工具都能够自动检测出来,如果你集成一个类在构造器中必然会有super();
这样一行。如果是带参的话也是一样的。
继承与组合
从上面的小节中可以初步的了解继承的一些特性,这里我想先和大家讨论一个问题,既然我继承了基类,构造器会帮我实例化一个隐藏的基类对象,那为什么我不直接实例化一个显示的基类对象(也还有一种情况,就是如果你不需要继承,你可以直接把这个类组合进你当前类中。)?
这个问题的核心是:如何去选择继承与组合?其实我们日常的开发中,动不动就选择继承,我们并没有考虑过盲目的选择继承到底符不符合设计原理。下面的三种情况请大家仔细思考一下:
- 如果我需要一个类,但是我目前只能确定一部分的功能,并且我也希望他有一些衍生类。那我该不该用继承?
我个人认为如果你是这种情况,可能接口的实现方式很适合你。因为接口的扩展性要比继承来的好。并且接口的副作用要比继承小。这一点在下一节会提到。 - 如果我需要一个类,但是我这个类中可能要调用另外一个类的大部分方法,我该不该用继承?
我认为可能这种情况并不一定要先考虑继承,可以先考虑组合,如果你确定要重写大量的功能的话,那么就用继承,但是如果你这个类日后要扩展的话还是仔细考虑一下接口。 - 如果我需要一个类,并且他会有很多衍生类,需要重写很多方法,但是这个类的行为已经确定了,不会有改动。那我需不需要用继承?
是的这种时候,继承会比任何结合形式都要好。就例如上一节的例子中提到的Shape
与Circle
的关系。还可能有Square
等等各种形状。Shape的行为和属性基本都是确定的。其他的形状都是属于Shape
是一种is-a
的关系。这种关系可以是的我们的多态特性极好的展现出来。
继承的坑
为什么说要慎重的去考虑是否一定要优先选择继承呢?请看下面代码:
//Sample_2.java
import me.hades;
import static me.hades.Utils.*;
class UseFul{
public UseFul(){
println("before init()");
init(); // 初始化方法
println("after init()");
}
protected void init(){
println("I'm UseFul.init()");
}
}
class MoreUseFul extends UseFul{
private int i = 1;
public MoreUseFul(int x){
i = x;
println("In Constructor the i = "+i);
}
@Override
protected void init(){
println("In MoreUseFul.init() the i = " + i);
}
}
public class Sample_2{
public static void main(String args[]){
new MoreUseFul(10);
}
}
思考一下这段代码,会是怎样的结果?
结果应该是这样的:
before init()
In MoreUseFul.init() the i = 0
after init()
In Constructor the i = 10
惊不惊喜?意不意外?为什么明明已经初始化过i了 还是显示 i = 0,后面有显示 i = 10。为什么不是调用基类的init方法啊?
一个个解释:
1. 为什么第一次的i打印是0而第二次打印10?
这里要说一下一个类的初始化顺序:实例化一个类->构造器->域属性->各种方法装载。一个类实例化的时候里面的所有属性会被初始化成二进制的0。这就是为什么会打印0。剩下的别急,请看下一个。
2. 为什么不调用基类的init()
方法?
这是因为在子类中你重写了init()
方法,那么就会导致基类的隐式对象只能调用你重写的init()
方法,由于初始化的顺序,基类的构造器先行于子类的构造器,所以此时i还是等于0.这就是为什么第一次打印出0。接着到了子类的构造器。这时构造器已经给i赋值为10,自然下一句打出的i就是等于10。
所以在使用继承这种特性的时候希望能考虑一下上面提到的一些情况,如果想避免第三节中提到的情况,可以将public void init()
改为final void init() or private void init()
这些都是防止覆盖的。但是如果是init()
函数都不能覆盖,那么有必要用到继承吗?
最后祝大家,工作顺利,学习愉快。