可以回顾一下上一章中Employee类。某个公司中,经理和普通雇员的待遇存在差异,但是却也有很多相同的地方,例如都领取薪水,但普通雇员只有薪水,而经理还有业绩奖金。那么我们可以想到,经理也是一个雇员,但却比普通雇员多了某些属性。这种情况就是“is-a”的关系,就可以用到继承。
(一)定义子类
下面是Manager继承Employee的格式,使用extends关键字。
public class Manager extends Employee{
//添加域和方法
}
关键字extends表明正在构造的新类派生于一个已存在的类,这个已存在的类可以称为超类(superclass),基类(base class)或父类(parent class);新类称为子类(subclass),派生类(derived class)或孩子类(child class)。
尽管Employee是超类,但并不是因为它优于子类或者比子类拥有更多的功能,恰恰相反,子类比超类拥有的功能更加丰富。
在Manager类中,增加一个用于存储奖金的域,并且添加一个用于设置这个域的方法。
public class Manager extends Employee{
private double bonus;
...
public void setBonus(double bonus){
this.bonus = bonus;
}
}
Manager类的setBonus方法,Employee类的对象是不能使用的。但是由于Manager类继承于Employee类,所以事实上Manager类除了bonus域还有另外三个域:name,salary,hireDay,所以Manager类对象就有4个域:name,salary,bonus,hireDaty。那么在方法上也是同理,Manager类对象可以使用getName和getHireDay方法。
综上所述,我们在设计超类时,就应该将通用的方法放在超类中,而将有特殊用途的方法放在子类中。
(二)覆盖方法
在Java中使用继承的时候,超类中的有些方法对子类并不一定适用。例如:Manager类中的getSalary方法应该返回的是salary和bonus的总和,所以此时我们需要提供一个新的方法来覆盖(override)超类中的这个方法。
public double getSalary() {
return bonus+salary;
}
乍看起来可以这么写,只要返回bonus和salary的总和,但是此方法却不能运行。因为Manager类的getSalary方法不能直接访问超类中的私有域。也就是说因为Employee类的salary域是私有的,任何其他类对象都不能访问另一个类的私有域。那么是不是可以这样写呢?
public double getSalary() {
double baseSalary = getSalary();
retrun baseSalary+bonus;
}
事实证明,上面这段代码也不能运行。因为Manager类自己也有一个getSalary方法,也就意味着Manager类的getSalary方法中还在调用自己,所以这条语句将会导致无限次地调用自己,直到整个程序崩溃为止。那么如何使我们调用的getSalary方法是Employee类的,而不是自己的。为此,我们需要使用super关键字。
public double getSalary() {
double baseSalary = super.getSalary();
return baseSalary+bonus;
}
在子类中,可以增加域,增加方法或者覆盖超类的方法,但绝不能删除继承的域或方法。
(三)子类构造器
我们知道构造器的作用是用来初始化对象,那么在继承中,我们也需要为子类添加构造器。由于子类不能访问超类的私有域,所以我们需要使用超类的构造器来对这部分私有域来进行初始化。同样可以说使用super关键字来调用超类的构造器。
注意:如果子类没有显示的调用超类的构造器,那么子类将自动调用超类的无参构造器。如果超类没有无参构造器,并且在子类中又没有显示地调用其他构造器,那么Java编译器将会报错。
public Manager(String name,double salary,int year,int month,int day){
super(name,salary,year,month,day);
bonus=0;
}
super关键字有两个用途:①调用超类的方法,②调用超类的构造器(在调用构造器时,只能作为另一个构造器的第一条语句出现。)
package CoreJava;
import java.time.LocalDate;
public class Emp {
private String name;
private double salary;
private LocalDate hireDay;
public Emp(String name,double salary,int year,int month,int day) {
this.name=name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void raiseSalary(double byPercent) {
double raise = salary*byPercent/100;
salary+=raise;
}
}
package CoreJava;
public class Manager extends Emp {
private double bonus;
public Manager(String name,double salary,int year,int month,int day) {
super(name,salary,year,month,day);//super出现在构造器的第一句
bonus=0;
}
public double getSalary() {
double baseSalary = super.getSalary();
return bonus+baseSalary;
}
public void setBonus(double b) {
bonus = b;
}
}
//super出现在构造器的第一句
bonus=0;
}
public double getSalary() {
double baseSalary = super.getSalary();
return bonus+baseSalary;
}
public void setBonus(double b) {
bonus = b;
}
}
package CoreJava;
public class ManagerTest{
public static void main(String[] args) {
Manager boss = new Manager("Carl Cracker",80000,1987,12,15);
boss.setBonus(5000);
Emp[] staff = new Emp[3];
staff[0] = boss;
staff[1] = new Emp("Harry Hacker",50000,1989,10,1);
staff[2] = new Emp("Tommy Tester",40000,1990,3,15);
for(Emp e : staff) {
System.out.println("name="+e.getName()+",salary = "+e.getSalary());
}
}
}
(四)继承层次
继承并不仅限于一个层次。由一个超类派生出来的所有类的集合被称为继承层次。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链。
通常一个祖先类可以拥有多个子孙继承链。
(五)多态
有一个规则可以用来判断是否应该设计为继承关系,就是“is-a”规则。它表明子类的每个对象也是超类的对象。例如:每个经理都是雇员,但不是每个雇员都是经理。
在Java中,对象变量是多态的。一个超类变量既可以引用该类的对象,也可以引用该类任何一个子类的对象。在上个例子中,staff[0]和boss引用同一个对象,但是编译器将staff[0]看成是Emp类对象,也就是说可以这样调用:
boss.setBonus(5000);//OK
但是不能这样调用:
staff[0].setBonus(5000);//Error 因为staff[0]声明的类型是Emp,而setBonus不是Emp类的方法
但是不能将一个超类的引用赋给子类变量。原因很简单,不是每个雇员都是经理。
(六)阻止继承:final类和方法
有时候我们希望某个类不能被继承。那么我们可以使用final修饰符来修饰这个类。
public final class Executive{
...
}
类中特定的方法也可以被声明为final,如果这样做,那么子类就不能覆盖这个方法(final类中所有的方法都自动成为final方法)。
public class Employee{
...
public final String getName(){
return name;
}
}
(七)强制类型转换
我们在基本类型数据中见到过强制类型转换,是指大转到小,这样带来的结果可能是丢失精度。在对象变量也可以使用强制类型转换,但是应该满足以下条件:
①只能在继承层次类进行类型转换
②在将超类转换成子类之前,应该使用instanceof进行检查
在一般情况下,需要使用强制类型转换去调用某个方法时,就应该考虑超类的设计是否合理。应该尽量少用类型转换和instanceof运算符
(八)抽象类
如果自下而上在类的继承层次中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某个角度看,祖先类应该更加通用,我们可以只把它作为派生其他类的基类,而不作为想使用的特定的实例类。
抽象类使用abstract关键字,抽象类中有抽象方法,但是还可以包含具体数据和具体方法。我们建议尽量将通用的域和方法(不管是否是抽象的)放在超类(不管是否是抽象类)中。
注意:①抽象类中可以没有抽象方法,但是抽象方法一定要在抽象类中。
②抽象类不能被实例化。也就是说,如果一个类声明为abstract,就不能创建这个类的对象。
package abstractClasses;
public abstract class Person {
public abstract String getDescription();
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
package abstractClasses;
import java.time.LocalDate;
public class Employee extends Person{
private double salary;
private LocalDate hireDay;
public Employee(String name,double salary,int year,int month,int day) {
super(name);
this.salary=salary;
hireDay = LocalDate.of(year, month, day);
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public String getDescription() {
return String.format("an employee with a salary of $%.2f", salary);
}
public void raiseSalary(double byPercent) {
double raise = salary*byPercent/100;
salary += raise;
}
}
package abstractClasses;
public class Student extends Person {
private String major;
public Student(String name,String major) {
super(name);
this.major = major;
}
public String getDescription() {
return "a student majoring in "+major;
}
}
package abstractClasses;
public class PersonTest {
public static void main(String[] args) {
Person[] people = new Person[2];
people[0] = new Employee("Harry Hacker",50000,1987,10,1);
people[1] = new Student("Maria Morris","computer science");
for(Person p : people) {
System.out.println(p.getName()+","+p.getDescription());
}
}
}
(九)受保护的访问
一般情况下,我们建议将域标记为private,将方法标记为public,在继承中,就算子类也不能访问超类的私有域。然而,在有些时候,我们希望超类中的某些方法允许被子类访问,或者允许子类的方法访问超类的某个域。为此,可以将这些方法或域声明为protected。
Java中用于控制可见性的4个访问修饰符:
①仅对本类可见——private
②对所有类可见——public
③对本包和所有子类可见——protected
④对本包可见——默认,不需要修饰符