在前一章中的 Employee 类(雇员),公司中不只有雇员,还有 Manager (经理),他们都领薪水,而经理完成了预期的业绩之后还能得到奖金。我们可以看到,经理可以从雇员中继承部分代码。Manager 和 Employee 之间存在明显的 is-a (是)关系,每个经理都是一名雇员。

定义子类

我们可以根据上述的描述编写 Manager 类

  1. public class Manager extends Employee
  2. {
  3. }

上述表明正在构造的新类派生与一个已存在的类。
已存在的类称为超类(superclass)、基类(base class)或父类(parent class)。
新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。
尽管 Employee 类是一个超类,但并不是因为它优于子类或者拥有比子类更多的功能。实际上恰恰相反,子类比超类拥有的功能更加丰富。例如,读过 Manager 类的源代码之后就会发现,Manager 类比超类 Employee 封装了更多的数据,拥有更多的功能。

前缀“超”和“子”来源于计算机科学和数学理论中的集合语言的术语。所有雇员组成的集合包含所有经理组成的集合。可以这样说,雇员集合是经理集合的超集,也可以说,经理集合是雇员集合的子集。

现在实现 Manager 类,增加一个用于存储奖金信息的域,以及一个用于设置这个域的新方法:

  1. public class Manager extends Employee
  2. {
  3. private double bonus;
  4. ...
  5. public void setBonus(double bonus) {
  6. this.bonus = bonus;
  7. }
  8. }

如果有一个 Manager 对象,就可以使用 setBonus() ,但是 Employee 对象不能使用,因为 setBonus() 不是在 Employee 类中定义的。
虽然 Manager 类没有显式地定义 getName()getHireDay() 等方法,但是 Manager 类的对象却可以使用他们,这是因为 Manager 类自动继承了超类 Employee 类中的方法。域也是同样道理。

在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。

覆盖方法

由于 Manager 类中 bonus 域的缘故,所以 Manager 类要重写 getSalary() ,这在程序设计语言中叫覆盖(override)。
虽然 Manager 类继承超类的域,但是由于域是私有域,所以 Manager 不能直接的范围它,这里指 salary :

  1. public double getSalary()
  2. {
  3. return salary + bonus; // won't work
  4. }

但是可以借助 Employee 类中的公共接口(getSalary())访问。当然,要加上 super 关键字:

  1. public double getSalary()
  2. {
  3. double baseSalary = super.getSalary();
  4. return baseSalary + bonus;
  5. }

如果上述没有加上 super ,将会调用 Manager 类的 getSalary() 从而递归调用自己陷入死循环。如果不想加 super 可以将 Manager 类的 getSalary() 该名即可。

super 与 this 引用不一样。super 不是一个对象的引用,不能将 super 赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

子类构造器

由于 Manager 类的构造器不能访问 Employee 类的私有域,所以必须利用 Employee 类的构造器对这部分私有域进行初始化:

  1. public Manager(String name, double salary, int year, int month, int day) {
  2. super(name, salary, year, month, day);
  3. bonus = 0;
  4. }

注意:super 调用构造器的语句必须是子类构造器的第一条语句。

如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。
Manager 已经构建的差不多了,现在来编写一些代码:

  1. Manager boss = new Manager("Carl Cracker", 80000, 1985, 12, 16);
  2. boss.setBonus(5000);
  3. Employee[] staff = new Employee[2];
  4. staff[0] = boss;
  5. staff[1] = new Employee("Harry Hacker", 50000, 1987, 10, 3);
  6. for (Employee e : staff) {
  7. System.out.prinln(e.getName() + " " + e.getSalary());
  8. // Carl Cracker 85000.0
  9. // Harry Hacker 50000.0
  10. }

可以看到 e.getSalary(),这里将 e 声明为 Employee 类型,但实际上,e 既可以引用 Employee 类,也可以引用子类 Manger 类。当 e 引用 Employee 对象时,e.getSalary() 调用的是 Employee 类中的 getSalary 方法;当 e 引用 Manager 对象时,e.getSalary() 调用的是 Manager 类中的 getSalary 方法.
一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)

多态

判断是否应该设计为继承关系的简单规则为 is-a 规则,它表明子类的每个对象也是超类的对象。例如,每个经理都是雇员。所以 Manager extends Employee
is-a 的另一种表述法是 置换法则(里氏替换原则)。它表明程序中出现超类对象的任何地方都可以用子类对象替换:

  1. Employee e;
  2. e = new Employee(...); // Employee object expected
  3. e = new Manager(...); // OK, Manager can be used as well

在 Java 程序设计语言中,对象变量是多态的。一个 Employee 变量既可以引用一个 Employee 类对象,也可以引用一个 Employee 类的任何一个子类的对象,like:Manager。
多态也要注意一些点:

  1. Manager boss = new Manager(...);
  2. Employee[] staff = new Employee[3];
  3. staff[0] = boss;

上面这个例子中,staff[0] 是 Employee 对象,所以他不能访问 Manager 对象的域和方法:

  1. boss.setBonus(5000); // OK
  2. staff[0].setBonus(5000); // Error

如果要访问,就必须要将 Employee 对象转换成 Manager 对象,并且要加上强制转换符:

  1. Manager m = staff[0]; // Error
  2. Manager m = (Manager) staff[0]; // OK

上述转换的前提是 staff[0] 就是一个 Manager 对象,否则即使编译能够通过,虚拟机也会抛出异常。
超类数组引用子类数组也要注意,不要使用下面的例子。

  1. Manager[] managers = new Manager[10];
  2. Employee[] staff = managers;
  3. staff[0] = new Employee("Harry Hacker", ...); // throw ArrayStoreException
  4. // staff[0].setBonus is error

理解方法调用

在对象上调用方法的过程非常重要。现在有一个继承自 D 类 的 C 类,他有一个隐式参数为 x 的对象。它将调用 x.f("kang")

  1. 编译器找出 C 类中名为 f 的方法。超类中名为 f 的共有方法。
  2. 查看调用方法中的参数类型,是否和调用的参数类型匹配。
  3. 如果是 private 方法、static 方法、final 方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
  4. 动态绑定方法时,如果在 C 类中定义了 f(String),就直接调用它,否则,尽在超类 D 中寻找 f(String) 依次类推。

每次调用方法都要搜索,所以虚拟机为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。
例如,Employee 的方法表为:

  1. Employee:
  2. getName() -> Employee.getName()
  3. getSalary() -> Employee.getSalary()
  4. getHireDay() -> Employee.getHireDay()
  5. raiseSalary(double) -> Employee.raiseSalary(double)

阻止继承:final 类和方法

不允许拓展的类被称为 final 类,将方法声明为 final,子类就不能覆盖这个方法。
将一个类声明为 final,只有其中的方法自动称为final,不包括域。
将方法或类声明为 final 主要目的是:确保它们不会在子类中改变语义。String 类是 final 类,这意味着不允许任何人定义 String 的子类。换言之,如果有一个 String 的引用,它引用的一定是一个 String 对象,而不可能是其他类的对象
### 强制转换类型
进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。
将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能够通过运行时的检查。
有些时候强制转换会造成错误,因为可能两个对象并不相同:

  1. Manager boss = (Manager) staff[1] // Error

删除会产生 ClassCastException 异常,为了避免上述异常,可以使用 instanceof 操作符:

  1. if (staff[1] instanceof Manager) {
  2. boss = (Manager) staff[1];
  3. }

综上所述:

  • 只能在继承层次内进行类型转换。
  • 在将超类转换成子类之前,应该使用instanceof进行检查。

    抽象类

    如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。例如,一个雇员是一个人,一个学生也是一个人。也就可以看成 Person 类有两个子类:Student 类和 Employee 类。
    Person 类中有一些方法是无法实现的,比如 getDesrciption() 具体的描述,因为 Person 类中信息过少。可以将这些方法使用 abstract 关键字,将这些方法定义为抽象,这样就不用实现这个方法了。这样包含一个或多个抽象方法的类本身必须被声明为抽象的。 ``` public abstract class Person { private String name; public Person(String name) {

    1. this.name = name;

    }

    public abstract String getDescription();

    public String getName() {

    1. return name;

    } }

  1. 扩展抽象类可以有两种选择:
  2. - 一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;
  3. - 另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。
  4. > 类即使不含抽象方法,也可以将类声明为抽象类。
  5. 抽象类不能被实例化,但是可以创建一个具体子类的对象:

Person p = new Student(“yikang”);

```

只能引用非抽象子类的对象。

受保护访问

大家都知道,最好将类中的标记为 private,而方法标记为 public。
然而,在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected。例如,如果将超类 Employee 中的 hireDay 声明为 proteced,而不是私有的,Manager 中的方法就可以直接地访问它。
不过,Manager 类中的方法只能够访问 Manager 对象中的 hireDay 域,而不能访问其他 Employee 对象中的这个域。这种限制有助于避免滥用受保护机制,使得子类只能获得访问受保护域的权利。
Java 中的 4 个访问修饰符:

  1. 仅对本类可见 -> private
  2. 对所有类可见 -> public
  3. 对本包和所有子类可见 -> protected
  4. 对本包可见 -> 默认,不需要修饰符(package-private)