一.概述
上一篇讲了字符串API的使用。
这篇我们开始讲Java三大特性的第二大特性——继承。
通过继承可以构建父子关系的类,让子类可以继承父类的方法以及实例域,这让代码更加的具有复用性。也正因为有了继承才演变了出了第三个特性,多态,字面意思我们可以理解为多么变态。他确实足够的变态,他可以编程各种各样的姿态来运行,达到更大的复用。为接口和抽象类奠定了基础。
二.继承
1.父子类
原来写过员工类,现在写一个经理类,而经理类是员工类的子类,反之,员工类就是经理类的父类。也可以说经理类继承自员工类。有些人就有疑问了?为什么经理的职位比员工高,而经理类是子类呢??这时候就要使用类关系中的继承了,继承表示的是is-a的一种强关系。经理是一名员工成立,反之员工是一名经理。很明显这样是不成立的。所以也更能看出,子类是扩展了父类的功能。父类也称作超类,基类。子类也称作派生类,孩子类。这些都是继承时使用的术语。
我们通过员工,经理之间的实例demo来看一看继承的用法:
/**
* 员工类
*/
public class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void setHireDay(LocalDate hireDay) {
this.hireDay = hireDay;
}
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
}
public void raiseSalary(double byPercent){
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", salary=" + salary +
", hireDay=" + hireDay +
'}';
}
}
/**
* 经理类继承自员工类
*/
public class Manager extends Employee{
/**
* 这里虽然没有写名称等实例域,但是由于继承的原因我们也就相当拥有了Employee的实例域变量。
*/
/**
* 奖金 这是子类Manager独有的属性
*/
private double bonus;
public Manager(String name, double salary, int year, int month, int day, double bonus) {
// 由于子类不能访问父类的私有属性所以我们不能通过构造器给父类的实例变量赋值为默认值,所以必须通过调用父类的构造器来进行初始化
// 如果父类含有默认的构造器,默认调用,如有没有,则必须显示调用有参构造。并且和this()调用重载构造只能存在一个。
// 子类的构造器调用了父类的构造器
super(name, salary, year, month, day);
this.bonus = bonus;
}
/**
* 覆盖父类的方法,要求
* 1.方法签名相同
* 2.返回值类型相同
* @return
*/
public double getSalary() {
// 虽然子类具有父类的私有属性,只是代表当前对象也具有父类的状态,但是并不能直接使用。
// 通过父类的getSalary()方法,但是子类覆盖了父类的方法。如果想要使用就要通过super关键字调用父类的方法来获取。
// 这里注意和this隐式参数的区分,super并没有特殊含义,只是告诉编译器要调用父类的方法。
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
}
/**
* 测试 经理类和员工类的关系
*/
public class ManageTestDemo {
public static void main(String[] args) {
Manager boss = new Manager("Carl",100,2000,1,1,100);
boss.setBonus(200);
Employee[] employees = new Employee[3];
// 虽然是一个员工数组,但是经理同属于员工,所以员工存在于员工数组中也是合情合理
employees[0] = boss;
employees[1] = new Employee("Harry",200,1999,1,1);
employees[2] = new Employee("Tony",200,1999,2,2);
// 在打印中经理调用的getSalary方法结果是经理类的方法,这就是Java中的多态机制,在运行时确定对象类型并自动的选择调用对应方法。
for (Employee employee : employees) {
System.out.println(employee);
}
}
}
2.继承层次
由公共父类派生出来的所有子类的集合,被称为继承层次,从某个特定的类到其祖先的路径被称为该类的继承链。Java不支持多继承。如果想要阻止继承,可以把类定义成final类,用final标记的类称为不可变类,比如讲过的String类。当想要方法不被子类覆盖这个方法,就可以使用final修饰。如果是不可变类,那么当前类的方法都会被默认成为final,但不包含实例域,并且在其生成当前类的对象时,只能是当前类,不能是其子类对象。
3.内联
如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程称为内联。例如内联调用getXXX某个域的访问器,将被替换成直接访问对象属性。虚拟机的即时编译器可以准确的知道类之间的关系。并能够检测出来类中是否真正的存在覆盖给定的方法。如果很简短并且被频繁调用没有被覆盖,即时编译器就会将这个方法进行内联处理。如果当虚拟机加载了一个子类,并进行了内联方法的覆盖,那么即时编译器会取消对该方法的内联操作。
4.强制类型转换
看过基本类型转换,对象也存在类型转换,转换方式和基本类型一样。进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的实际类型之后,使用对象的实际类型之后,使用对象的全部功能。
/**
* 类型转换
*/
public class CastDemo {
public static void main(String[] args) {
Employee employee = new Employee();
Manager manager = new Manager();
// 但是这种转换是不安全的,会终止程序的运行,所以在转换之前要做检查
if (manager instanceof Employee){
// 当我们想使用子类Manage类的方法时,需要将父类转换成子类才能调用
manager = (Manager) employee;
}
}
}
总结:
- 只能在继承层次内进行类型转换。
- 在父类转换成子类之前,应该使用instanceof进行检查。
三.多态
1.基本概念
Java中的第三个特性就是多态。用来判断是否应该设计为继承关系的规则是“is-a”来判断。它的另一种表述法是置换法则,它表明程序中出现父类的任何地方都可以使用子类来代替。无需对现存代码进行修改,就可以对程序进行扩展,这就是多态的特性。我们只需定义一个类来继承父类,并且覆盖父类的方法,声明变量时,引用子类类型对象,在调用方法的时候无需改变,虚拟机就会查找对应实际类型的方法。
/**
- 多态
*/
public class PolymorphicDemo {
public static void main(String[] args) {
// 员工对象变量可以是员工对象
Employee employee = new Employee("Tony",100,1998,10,1);
// 员工对象变量也可以是经理对象
Employee manager = new Manager("Tony",100,1998,10,1,100);
// 通过结果我们看出同一个类型调用同一个方法,但是结果却是不同的。
System.out.println(employee.getSalary());
System.out.println(manager.getSalary());
}
}
2.理解方法调用过程
(1)编译器查看对象的声明类型和方法名。
编译器会一一列举所有Employee类中名为getSalary的方法和其父类中访问属性为public且名为getSalary的方法(父类中的私有方法不可访问)。
(2)编译器将查看调用方法时提供的参数类型。
从查询出的所有方法中选用提供的参数类型完全匹配的。这个过程就是重载解析。如果没有匹配的,编译器也会自动将类型转换并查询有无匹配的方法。否则将会报告错误。
(3)查看是否是静态绑定还是动态绑定。
如果是private方法,static方法,final方法或者构造器,编译器将可以准确的知道应该调用哪个方法,这种称为静态绑定。如果依赖隐式参数的实际类型,并且在运行时绑定称为动态绑定。实例中的隐式参数就是employee和manager,他们调用的方法就是动态绑定。
(4)当程序运行,并且采用动态绑定方法调用时,虚拟机一定调用与employee和manager所引用对象的实际类型最合适的哪个类的方法。
manager的实际类型是Manager它是Employee的子类,如果Manager定义了getSalary方法,那么就直接调用,否则查找父类的getSalary方法进行调用,以此类推。
3.方法表
每次调用,虚拟机都会去查询当前可调用的所有方法,导致系统时间开销很大。因此,虚拟机预先为每个类创建了一个方法表,列出了所有方法的签名和实际调用的方法。在真正调用的时候直接查询方法表就可以了。
四.访问操作符
private和public前面已经知道了,一个表示私有,一个表示公共。当我们使用继承的时候可以使用protected访问修饰符,如果我们在父类中定义的方法只想提供给子类使用和本包其他类而不想提供给其它类,这时候就使用这个修饰符。不推荐使用在实例域上。
- 仅对本类可见——private
- 对多有类可以——public
- 对本包和多有子类可见——protected
- 对本包可见——不使用修饰符
五.继承的设计技巧:
- 将公共操作和域放在超类
- 不要使用受保护的域
- 使用继承实现“is-a”关系
- 除非所有继承的方法都有意义,否则不要使用继承。
- 在覆盖方法时,不要改变预期的行为。
- 使用多态,而非类型信息。
六.结束语
继承并不是我们用的最多的地方,过多的继承会导致关系混乱,当我们想要达到更高的复用,往往使用组合的更多一些。所以推荐新手不要为了复用而不关心类之间的关系而乱用继承。而多态是我们平常使用最多的,因为我们往往都在使用别人设计好的继承关系和自己定义接口。所以多态的理解更重要一些。
下一篇讲解万物皆对象的Object类。
有些可能我理解的不够深刻,大家如果觉得我说的不够详细可以参考我的推荐书,详细的看一下。欢迎大家评论。第一时间我会回复大家。谢谢!