5.8 继承与组合 5.8.1 使用继承的注意点

5.8 继承与组合

继承是实现类复用的重要手段,但继承带来了一个最大的坏处:破坏封装
相比之下,组合也是实现类复用的重要方式,而采用组合方式来实现类复用则能提供更好的封装性。

下面将详细介绍继承和组合之间的联系与区别。

5.8.1 使用继承的注意点

子类扩展父类时,子类可以从父类继承得到成员变量和方法,如果访问权限允许,子类可以直接访问父类的成员变量和方法,相当于子类可以直接复用父类的成员变量和方法。

继承会破坏父类的封装性

继承带来了高度复用的同时,也带来了一个严重的问题:继承严重地破坏了父类的封装性。

前面介绍封装时提到:每个类都应该封装它内部信息和实现细节,而只暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的成员变量(内部信息)和方法,从而造成子类和父类的严重耦合。

从这个角度来看,父类的实现细节对子类不再透明,子类可以访问父类的成员变量和方法,并可以改变父类方法的实现细节(例如,通过方法重写的方式来改变父类的方法实现),从而导致子类可以恶意篡改父类的方法。

父类设计规则

为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则

  • 尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置成private访问类型,不要让子类直接访问父类的成员变量
  • 不要让子类可以随意访问、修改父类的方法。
    • 父类中那些辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该工具方法;
    • 如果父类中的方法需要被外部类调用,则必须以public修饰,
      • 如果不希望子类重写该方法,可以使用final修饰符来修饰该方法;
    • 如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。
  • 尽量不要在父类构造器中调用将要被子类重写的方法

父类构造器调用被重写方法时容易发生错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base {
public Base() {
// 如果该方法被子类重写,则会调用重写的test方法
test();
}

public void test() // ①号test()方法
{
System.out.println("将被子类重写的方法");
}
}

public class Sub extends Base {
private String name;

// 重写父类的方法
public void test() // ②号test()方法
{
// name没有设置将会触发空指针异常
System.out.println("子类重写父类的方法," + "其name字符串长度" + name.length());
}

public static void main(String[] args) {
// 下面代码会引发空指针异常
Sub s = new Sub();
}
}

上面的代码中,子类增加了父类没有的name属性,并重写的父类的test方法,子类重写的test方法会隐藏父类的同名方法
在创建子类时,会先调用父类的构造器,

  • 由于在父类构造器中调用了test方法,
  • 由于子类重写的test方法会隐藏父类的同名方法,

所以父类构造器调用的是子类重写的test方法,子类重写的test方法需要调用子类新增的成员变量name,但该name成员变量没有初始化,此时引用变量name的值为null,所以name.length()方法会引出空指针异常.

什么时候需要派生子类

从父类派生新的子类需要具备以下两个条件之一。

  1. 子类需要额外增加属性,而不仅仅是属性值的改变。例如从Person类派生出Student子类, Person类里没有提供grade年级)属性,而Student类需要grade属性来保存Student对象就读的年级,这种父类到子类的派生,就符合Java继承的前提。
  2. **子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)**。例如从Pcrson类派生出Teacher类,其中Teacher类需要增加一个teaching0方法,该方法用于描述Teacher对象独有的行为方式:教学。