前言
李刚老师《JAVA疯狂讲义》第5版,第5章学习笔记。
1.JAVA中继承的局限性
JAVA中的继承是实现代码复用的重要手段,但是继承的一个最大问题是会破坏封装性,子类可以访问父类的成员变量和方法,父类的内部实现细节对子类不再封闭。
为了尽量避免这一情况的出现,一方面应遵守一定的父类设计规范,另一方面,可以借助组合来实现代码复用。
父类设计规范包括:
- 尽量隐藏父类内部数据,用private修饰父类的成员变量。
- 父类中的工具方法(实现其他方法的方法)也应使用private修饰,以免子类访问修改。
- 若父类中的方法必须被外部类调用,必须用public修饰,但又不想被子类重写,则可使用finally修饰符修饰方法。
- 父类的构造器中,尽量不要使用会被子类重写的方法,因为在创建子类的对象时,也会调用父类的构造器。若父类构造器中包含了一个被子类重写的方法,则会返回子类中定义的方法结果,而不会返回父类中定义的方法结果。例如:
public class Base {
public Base() {
test();
}
public void test() {
System.out.println("我是Base的test方法");
}
}
public class Sub extends Base {
private String name;
public void test() {
System.out.println("我是Sub的test方法");
System.out.println(name);
}
}
public class Demo01 {
public static void main(String[] args) {
//将会输出:我是Sub的test方法
//null
Sub s = new Sub();
}
}
上方代码中,Sub是Base的子类,在新建一个sub对象是,首先调用Base的构造器:
public Base() {
test();
}
由于test()前面没有主语,因此是省略了this主语,this代指正在构造的这个对象,正在构造的这个对象是Sub,因此调用的test()方法将会是Sub类中的方法,而不会是Base中的test()方法。由于这个引用变量还没有构建完成,name还没有被赋值,所以就是null。
所以,绕来绕去,还不如就拒绝在父类的构造器中,引用被子类重写的方法!
2.JAVA中组合的实现
除了借助继承关系,利用组合关系也可实现代码的复用。它的基本思想是,直接把旧类对象作为新类的成员变量组合进来,这样就可以借助旧类的方法、成员变量实现新类的功能。并且,可以在旧类对象前加上private修饰符,这样可以实现更好的封装性。
代码举例如下:
public class Animal {
public void breath() {
System.out.println("俺会呼吸!");
}
}
public class Brid {
private Animal a;
public Brid(Animal a) {
this.a = a;
}
public void breath() {
a.breath();
}
public void fly() {
System.out.println("俺会飞!");
}
}
public class Demo01 {
public static void main(String[] args) {
Animal a = new Animal();
Brid b = new Brid(a);
//输出:俺会呼吸!
b.breath();
}
}
Brid类中定义了一个Animal类,并且Brid的构造器也使用Animal类的对象来创建。可以说,Brid类由Animal类组合而成,但Brid类并不是Animal类的子类。
注意:
虽然使用组合关系时,需要创建两个Animal对象,但是并不意味着,组合关系的实现比继承关系的实现需要的系统开销更大。因为在创建一个类的对象时,也会调用其所有父类的构造器,也就需要为所有父类的实例变量分配空间,因此,继承设计和组合设计的系统开销不会有过多差别。
那么到底什么时候使用继承,什么时候使用组合呢?
总体而言,继承是一种is a的关系,组合类似于一种has a的关系,例如,鸵鸟是一种鸟,那么鸵鸟类就应该继承鸟类。鸟有翅膀,那么鸟类和翅膀类就应该是一种组合关系。