Java基础知识点4:继承
继承是面向对象编程技术中非常重要的一个基本概念。它背后的基本思想就是:通过已有的类来创建一个新的类,这个新的类可以重用(或继承)已有的类方法;新的类也可以加入新的方法和属性。
在这里我们通过一个实例来讲解继承的基本知识。假设我们在为一个公司设计一个管理系统,管理公司的人员,我们首先定义了一个雇员类Employee,它的定义如下:
1 public Employee { 2 private String name; 3 private double salary; 4 5 public Employee(String name, double salary) { 6 this.name = name; 7 this.salary = salary; 8 } 9 10 public String getName() { 11 return name; 12 } 13 14 public void setName(String name) { 15 this.name = name; 16 } 17 18 public double getSalary() { 19 return salary; 20 } 21 22 public void setSalary(double salary) { 23 this.salary = salary; 24 } 25 26 }
基本语法
基本定义
定义一个继承自已有类的子类所涉及到的关键字是extends,基本语法如下:
子类名 extends 超类名 {
子类定义的新的属性和方法:
}
也就是说,定义一个继承自超类的子类,只需要声明二者不同的地方就可以了。
比如现在我们想为公司的经理们设计一个类Manager,由于经理也是雇员,所以雇员的基本属性经理也都有。但是经理可能有一些其他雇员没有的属性和方法,比如经理会有分红bonus。所以采用继承的方式来定义Manager类是最合适不过的。
1 public Manager extends Employee { 2 3 private double bonus; 4 5 public Manager(String name, double salary, double bonus) { 6 super(name, salary); 7 this.bonus = bonus; 8 } 9 10 public double getBonus() { 11 return bonus; 12 } 13 14 public void setBonus(double bonus) { 15 this.bonus = bonus; 16 } 17 18 }
这样我们就成功的建立了一个继承自Employee的新类Manager,这个Manager不仅仅具有Employee类的属性和方法,而且还包含自己独有的属性和方法。
关于子类这里有几点需要注意
1. 子类能够直接调用超类的public,protected方法。之所以使用protected,这样可以保证这个方法是可以被该超类的子类所应用的。但是不能被其他的类所使用。
2. 子类不能够访问超类中的private属性,方法。
3. 子类只能够通过public的接口去访问超类中的private属性。
覆盖超类方法
在实际情况中还会出现这样的需求,有些方法在超类中定义了,而且子类也需要使用这个方法。但是,子类对于这个方法的定义与超类的定义不一样。以上面的Manager类为例,Manager需要使用getSalary方法获取它的工资,在超类中的定义就是返回私有域salary的值,但是在Manager中,我们需要返回salary和bonus域的和。这里应该怎么去实现?
这里就需要Manager类去覆写(override)这个getSalary方法:
public double getSalary() { return salary + bonus; } public double getSalary() { double baseSalary = getSalary(); return baseSalary + bonus; } public double getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus; }
这里给出了三个版本,其中第一,二个都是错误的,只有第三个是对的。第一个错在不能直接访问超类中的private属性salary,必须调用超类的getSalary方法。第二个版本错在它调用的是子类的getSalary方法,不是超类的,所以这个方法会无限循环执行。只有第三个是正确的。
所以这里要记住:
在子类中可以定义一个声明完全和超类一样的方法,这个叫做方法的覆写,当子类再次调用这个方法时,那执行的就是子类中定义的版本。
另外,在子类中如果想要调用超类的方法,一定要在方法名字前加上super。
构造方法
子类的构造方法的写法也是需要注意的。由于子类是无法直接访问超类中的private的数据域的,所以对于这些域,如果你想通过构造方法来为它们设定初值时,你就需要首先调用超类中的带参数构造方法,然后再为子类中特殊的域设置初值。
比如我们写下Manager类的构造方法:
1 public Manager(String name, double salary, double bonus) { 2 super(name, salary); 3 this.bonus = bonus; 4 }
其中第一句super就是调用超类中的带参数构造方法。这里要注意,这句super语句如果想这么使用,一定要把它放在第一句的位置。
这里还有一点要注意:
如果子类中没有显示的调用超类的构造方法,那么其实子类就会触发超类的无参数构造方法。如果超类中并不含有一个无参构造方法的话,那么子类还不显示的调用超类其他构造方法,那么java编译器就会报错。
当前例子中Employee没有无参构造器,因为它已经有含参的了,所以如果在Manager中没有调用super方法,那么就会报错。所以这里的解决方案就是在Employee中再定义一个无参构造器。
多态性
子类的对象可以被超类变量所引用,比如我们声明一个Employee引用,它引用的是一个子类对象:
Employee boss = new Manager("Steve", 10000, 5000); System.out.println(boss.getSalary());
但是这里要注意:采用超类变量引用是无法调用子类中的独有方法的,比如上面的例子中boss变量不能调用setBonus方法。
然后我们再分析上例中第二句:
如果超类变量调用了被子类所覆写的超类方法,则此时要执行的是子类中定义的版本!
所以此时打印出来的是15000。说明即便Manager对象boss被Employee类变量所引用,但是JDK可以识别这个对象到底属于哪一类。所以才会输出正确的结果。
之所以能实现这一点就是归功于jdk的动态绑定技术。
动态绑定技术是在java程序,对象的某个方法被调用时发生的技术。当某个对象引用调用这个对象的某个方法时,将会发生以下几步:
1. 根据声明的方法名,遍历所有可能被调用的函数。
2. 再根据输入参数的类型,确定最可能匹配的函数。如果没有或者是有多个,java都会报错。
3. 如果方法是private,static,final或构造器时,那么被调用的方法直接执行,这个叫做静态绑定。否则的话,java会按照对象到底是属于什么类型来调用正确的方法。
那么如果想让Employee类型的变量boss,调用setBonus方法怎么办?
方法就是强制类型转换,把Employee类型的变量boss强制转换为Manager类型,如下
Manager newBoss = (Manager) boss;
newBoss.setBonus(4000);
强制类型转换之前一般会做一次检查,检查一下要被转换的变量是否真的是要强制转换的类型,如果不是的话,会产生异常。检查的方法就是用instanceof方法:
if(boss instanceof Manager) Manager newBoss = (Manager) boss;
final修饰符
如果不希望其他程序继承自自己的类,那么可以把这个类用final修饰符修饰,就可以防止其他类去继承它。
如果不希望子类覆写超类的方法,那么在超类中可以把这个方法用final修饰符修饰一下。
抽象类
有的时候可能出现这种情况,好几个类都继承自同一个类,他们还有一个共同的方法,但是每一个类实现这个方法的方式都不一样。当这种问题发生时,你把这个方法的声明写在超类中还是子类中都是有不好的地方。所以这个时候就需要抽象类,抽象方法出场了。
针对这种每个子类都有,但是却都实现方式不同的公有方法,我们可以把它在超类中声明成抽象方法即可。基本语法就是在这个方法的返回类型前面加上abstract修饰符。
public abstract 返回类型 方法名(输入参数表)
如果一个类中包含至少一个抽象方法的话,这个类也必须被声明为抽象类
public abstract 类名 { ... }
当然抽象类中不必所有方法都是抽象的,也可以有已经实现的。
抽象类不能被实例化,一个类即便没有抽象方法也可以被声明为抽象的。当然抽象类的变量还是可以指向其非抽象的子类的。