在开始介绍Java的继承和多态之前,我们首先举一个生活中的例子,一个公司有雇员(Employee),雇员有三个属性,姓名(name),薪水(salary),雇佣日期(hireDay),但是在所有的雇员中间有一部分是管理者(Manager),雇员有的属性,Manager同样也有,但是Manager比雇员多了一个属性奖金(bonus),从理论上讲Manager和Employee由着明显的”is - a”(是)的关系,即每一个Manager都是一个Employee。这样我们就得到一个基本的继承关系
class Manager extends Employee
{
//添加方法和域
}
关键字extends表明正在构造的新类派生于一个已经存在的类,已经存在的类被称为超类(superclass),基类(base class),或者父类(father class)。新类被称为子类(subclass),派生类(derived class)或者是孩子类(child class)。Employee类是一个超类,这并不代表着他有比子类更多的功能,相反的是子类比超类拥有更丰富的功能。
Manager类中比Employee类多了一个属性奖金。
class Manager extends Employee
{
//其他内容
private double bonus;
public void setBonus(double bonus)
{
this.bonus = bonus;
}
}
setBonus()方法不是定义的Employee类中,所以Employee不能使用setBonus()方法
但是Manager却可以使用Employee中的name,salary,hireDay的setter和getter。这是因为子类Manager从父类Employee类中继承了这仨个属性,同时也继承了setter和getter方法。虽然看上去Manager只有一个属性,实际上他有四个字段,一个自己的,还有三个来自父类。
在通过拓展超类定义子类的时候,仅仅需要指出子类和超类的不同之处,将通用的类放在超类中,特殊的属性和方法放在子类中。
然而我们考虑Employee类中salary的setter方法,普通员工的薪水和Manager的薪水是不同的,Manager的薪水还需要加上奖金。所以在这里我们要覆盖Employee中的getSalary()方法。
class Manager extends Employee
{
//省略 ......
public double getSalary
{
return salary + bonus;
}
}
但是这个方法是**错误**的,因为Manager类的getSalary方法不能直接访问超类的私有域,也就是说尽管每个Manager对象都拥有一个名为salary的属性,但在Manager类的getSalary方法不能直接方法salary属性,只有Employee类的方法才能直接访问私有部分,如果Manager一定要访问salary,就必须借助getter
class Manager extends Employee
{
//省略 ......
public double getSalary
{
double salary = getSalary();
return salary + bonus;
}
}//但是这个方法还是错的
这样子会导致一个stackover错误,因为会不断的递归调用自己的getSalary方法,最终程序崩溃
这里需要调用**super关键字**
正确写法是:
class Manager extends Employee
{
//省略 ......
public double getSalary
{
double salary = super.getSalary();
return salary + bonus;
}
}
**super**并不是一个对象的引用,不能将super赋值给一个对象变量,它只是指示编译器调用超类的一个方法的特有关键字
子类可以增加域,增加方法,覆盖超类方法,但是绝不可以删除任何继承的任何域或者方法
现在我们给Manager添加一个构造方法。构造方法中传姓名,薪水,和雇佣日期三个参数,并将奖金设置为0;
public Manager(String name,double salary,String hireDay)
{
super(name,salary,hireDay);
//这行代码调用超类含有name,salary,hireDay
//的构造方法,
//使用super调用父类的构造方法必须在子类构造
//方法的第一行
bonus = 0;
}
Manager boss = new Manager("boss",9000,1999-12-1);
boss.setBonus = 500;
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employ("em1",3000,"2000-12-12");
staff[2] = new Employ("em2",2500,"2000-12-12");
for(int i = 0; i < staff.length;i++)
{
System.out.println(staff[i].getName() + " " +staff[i].getSalary());
}
输出的结果是正确的,boss的薪水包含了奖金,em1,和em2的薪水就是工资。
staff[i].getSalary()能够确定该执行哪个getSalary()方法,尽管我们把他申明为一个Employee数组,但实际上,他既可以引用Employee类,也可以引用Manager类。当e引用为Employee的时候调用的是Employee的getsalary方法,引用为Manager的时调用的是Manager的getSalary方法。
**一个对象对象,可以引用多种实际类型的现象被称为多态(polymorphism),运行时能够自动地调用哪个方法的现场称为动态绑定。**
上述程序中staff[]数组中所有的引用都是多态的,可以指向Employee的任何子类,
继承层次:
Manager类中可能有CEO,CTO等等,Employee类中可能有Programmer
![这里写图片描述](https://img-blog.csdn.net/20150425011852550)
继承关系图
我们来看一段有意思的代码
Manager boss = new Manager(.......);
Employee[] staff = new Employee[3];
staff[0] = boss;//实际上编译器将属于Manager类
//的boss看成了Employee类
boss.setBonus(500);//这样子调用时可以的。
staff[0].setBonus(500);//这样子是错误的,这是因
//为staff[0]申明的类型是Employee,而
//setBonus并不是Employee的方法。
接下来再看一段比较容易犯错的代码
Manager[] managers = new Manager[7];
Employee[] staffs = managers;//这是合法的,因为
//Java中,子类数组的引用可以转换成超类数组的引用,而
//不需要强制转换
staff[0] = new Employee(.......);
//编译器接受这个赋值操作,但在这里staff[0],和
//managers[0]引用的是同一个对象,似乎我们把普通员工
//归属了经理。
//当我们调用
managers[0].setBonus(1111);
//将会导致调用一个不存在的实例域,今儿搅乱相邻的存储
//空间的内容。
为了确保不发生这类错误,所有数组都要记住创建他们的元素类型。病负责监督紧将类型兼容的引用存入到数组。
**对象执行方法的过程是非常重要的。Java中多态是通过动态绑定实现的**
**1).编译器查看对象的声明类型和方法名**
假设调用x.f(param),且隐式参数x声明为C类,需要注意的是C类可能有多个方法名为f的方法,但参数类型不一样。例如f(int),f(String),
编译器会一一列举所有C类中的方法和其超类中访问属性为public且方法名为f的方法
**2).编译器将查看调用方法的参数类型**
如果在所有方法名为f的方法中存在一个与提供参数类型完全匹配,就选择这个方法,这个过程被称为重载解析。对于调用f(1),编译器会选择f(int),而不是f(String),由于允许强制转换,(int可以转换成double,和float),所以这个过程可能很复杂,如果编译器没有找到与参数类型匹配的方法或者发现经过强制转换后有多个方法与之匹配就会报告一个错误
在java中方法的名字和参数列表称为**方法签名。**
3).如果是private,static,final或者构造器,那么编译器将能准确地知道调用哪个方法,这个被称作**静态绑定**。
4).当程序运行时,并且采用动态绑定调用方法,虚拟机一定调用与x所引用对象的实际类最合适的那个类的方法,假设x的实际类型是D,
它是C的子类。如果D定义了方法f(int)就直接调用,否则将在D类的超类中寻找f(int).
每次调用方法都要进行搜索,时间开销非常大,因为虚拟机预先为每个类都创建了一个方法表.在表中列出了所有的方法签名和实际调用的方法。这样子调用方法的时候,虚拟机查表就可以了。
**最后我们解释下staff[i].getSalary()方法的执行过程**
1)首先,虚拟机提取e的实际类型的方法表,可能是Employee也可能是Manager的方发表,或者Employee其他子类的方法表
2)虚拟机搜索方法签名为getSalary()的方法,此时虚拟机已经知道应该调用哪个方法
3)虚拟机调用这个方法