类的继承
继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Java的类继承具有单继承的特点,每个子类只有一个直接父类。
继承的特点
Java的继承通过extends关键字来实现,实现继承的类被称为子类。父类和子类的关系,是一种一般和特殊的关系。例如水果和苹果的关系,苹果继承了水果,苹果是水果的子类,继承语法如下:
修饰符class SubClass extends SuperClass
{
//类定义部分
}
子类是父类的扩展,子类是一种特殊的父类,子类不能获得父类的构造器,下面是一个示例:
父类:
public class Fruit
{
public double weight;
public void info()
{
System.out.println("我是一个水果!重"+ weight +"g!");
}
}
子类:
public class Apple extends Fruit
{
public static void main(String[] args)
{
//创建Apple对象
Apple a = new Apple();
//Apple对象本身没有weight Field
//因为Apple的父类有weight Field,也可以访问Apple对象的Field
a.weight = 56;
// 调用Apple对象的info方法
a.info();
}
}
上面的Apple类本来只是一个空类,它只包含了一个main方法,但程序中创建了Apple对象之后,可以访问该Apple对象的weight实例Field和info()方法,这表明Apple对象也具有了weight实例Field和info()方法,这就是继承的作用,Java最多只能有一个直接父类,但是可以有无数个间接父类:
class Fruit extends Plant{...}
class Apple extends Fruit{...}
重写父类的方法
形象表示重写父类:鸟类都包含了飞翔方法,其中鸵鸟是一种特殊的鸟类,因此鸵鸟应该是鸟的子类,因此它也将从鸟类获得飞翔方法,但这个飞翔方法明显不适合鸵鸟,为此,鸵鸟需要重写鸟类的方法,例如:
父类如下:
public class Bird
{
//Bird类的fly方法
public void fly(){
System.out.println("我在天空里自由自在地飞翔...");
}
}
子类:
public class Ostrich extends Bird
{
//重写Bird类的fly方法
public void fly(){
System.out.println("我只能在地上奔跑...");
}
public static void main(String[] args)
{
//创建Ostrich对象
Ostrich os = new Ostrich();
//执行Ostrich对象的fly方法,将输出"我只能在地上奔跑..."
os.fly();
}
}
执行上面程序,将看到执行os.fly()时执行的不再是Bird类的fly方法,而是执行Ostrich类的fly方法,因为Ostrich重写了Bird的fly方法。
方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。例如,下面代码是完全正确的:
class BaseClass
{
//test方法是private访问权限,子类不可访问该方法
private void test(){...}
}
class SubClass extends BaseClass
{
//此处并不是方法重写,所以可以增加static关键字
public static void test(){...}
}
super限定
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。如果需要在子类方法中调用父类中被覆盖的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法:
public class Ostrich extends Bird
{
//重写Bird类的fly方法
public void fly(){
System.out.println("我只能在地上奔跑...");
}
public void callOverridedMethod()
{
//在子类方法中通过super显式调用父类被覆盖的实例方法
super.fly();
}
public static void main(String[] args)
{
//创建Ostrich对象
Ostrich os = new Ostrich();
//执行Ostrich对象的fly方法,将输出"我只能在地上奔跑..."
os.fly();
os.callOverridedMethod();
}
}
通过callOverridedMethod方法的帮助,就可以让Ostrich对象既可以调用自己重写的fly方法,也可以调用Bird类中被覆盖的fly方法(调用callOverridedMethod方法即可)。
父类方法和子类方法之间也可能发生重载,因为子类会获得父类方法,如果子类定义了一个与父类方法有相同的方法名,但参数列表不同的方法,就会形成父类方法和子类方法的重载。
同样可以使用super访问父类中被子类覆盖的Field:
class BaseClass
{ public int a = 5;
}
public class SubClass extends BaseClass
{ public int a = 7;
public void accessOwner()
{
System.out.println(a);
}
public void accessBase()
{
//通过super来限定访问从父类继承得到的a Field
System.out.println(super.a); }
public static void main(String[] args)
{
SubClass sc = new SubClass();
//输出7
sc.accessOwner();
//输出5
sc.accessBase();
}
}
如果在某个方法中访问名为a的Field,但没有显式指定调用者,则系统查找a的顺序为:
- 查找该方法中是否有名为a的局部变量;
- 查找当前类中是否包含名为a的Field;
- 查找a的直接父类中是否包含名为a的Field,依次上溯a的所有父类,直到java.lang.Object类,如果最终不能找到名为a的Field,则系统出现编译错误。
class Parent {
public String tag = "疯狂Java讲义"; //①
}
class Derived extends Parent
{
//定义一个私有的tag实例变量来隐藏父类的tag实例变量
private String tag = "轻量级Java EE企业应用实战"; //②
}
public class HideTest
{
public static void main(String[] args)
{
Derived d = new Derived();
//程序不可访问d的私有变量tag,所以下面语句将引起编译错误
// System.out.println(d.tag); //③
// 将d变量显式地向上转型为Parent后,即可访问tag实例变量
//程序将输出:“疯狂Java讲义”
System.out.println(((Parent)d).tag); //④
}
}
上面程序的①行粗体字代码为父类Parent定义了一个tag实例变量,②行粗体字代码为其子类定义了一个private的tag实例变量,子类中定义的这个实例变量将会隐藏父类中定义的tag实例变量。
③行粗体字代码处试图通过d来访问tag实例变量时,程序将提示访问权限不允许。
调用父类构造器
子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码,类似于前面所介绍的一个构造器调用另一个重载的构造器。
不管我们是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次。子类构造器调用父类构造器分如下几种情况。
- 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。
- 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时即会调用父类构造器。
- 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
不管上面哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器……依此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器。
如果创建ClassB的对象,系统将先执行java.lang.Object类的构造器,再执行ClassA类的构造器,然后才执行ClassB类的构造器,下面一个例子解释:
class Creature
{
public Creature()
{
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature
{
public Animal(String name)
{
System.out.println("Animal带一个参数的构造器," + "该动物的name为" + name);
}
public Animal(String name , int age)
{
//使用this调用同一个重载的构造器
this(name);
System.out.println("Animal带两个参数的构造器," + "其age为" + age);
}
}
public class Wolf extends Animal
{
public Wolf()
{
//显式调用父类有两个参数的构造器
super("灰太狼", 3);
System.out.println("Wolf无参数的构造器");
}
public static void main(String[] args)
{
new Wolf();
}
}
上面程序的main方法只创建了一个Wolf对象,但系统在底层完成了复杂的操作。运行上面程序,看到如下运行结果:
Creature无参数的构造器
Animal带一个参数的构造器,该动物的name为灰太狼
Animal带两个参数的构造器,其age为3
Wolf无参数的构造器
从上面运行过程来看,创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果某个父类通过this调用了同类中重载的构造器,就会依次执行此父类的多个构造器。
实例
领导与员工
对于在同一家公司工作的领导和员工而言,两者是有很多共同点的。例如每个月都会发工资,但是领导除了基本工资之外,还有部分的分红奖金。此时,如果我们使用员工类来编写领导类就会省去很多重复代码,利用继承技术可以让领导类使用员工类中属性和方法。
1.创建Employee类
新建项目Employee,并在其中创建一个Employee.java文件。在该类的主方法中创建了三个属性,分别是name(员工姓名)、salary(员工工资)和date(入厂时间),并分别为其生成了getXXX()和setXXX()方法。核心代码如下所示:
import java.util.Date;
public class Employee {
private String name; // 员工的姓名
private double salary; // 员工的工资
private Date date; // 员工的入厂时间
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 Date getDate() { // 获取员工的入厂时间
return date;
}
public void setDate(Date date) { // 设置员工的入厂时间
this.date = date;
}
}
2.创建Manager类继承Employee类
然后创建一个名为Manager的类,该类继承Employee类。并在该类中定义一个bonus成员字段,表示领导者的奖金。核心代码如下:
public class Manager extends Employee {
private double bonus; // 经理的奖金
public double getBonus() { // 获得经理的奖金
return bonus;
}
public void setBonus(double bonus) { // 设置经理的奖金
this.bonus = bonus;
}
}
3.测试
测试类Test.java,在该类中分别创建Employee和Manager对象,并对其赋值:
package Employee;
import java.util.Date;
public class Test {
public static void main(String[] args) {
Employee employee = new Employee(); // 创建Employee对象并为其赋值
employee.setName("Lester");
employee.setSalary(1800);
employee.setDate(new Date());
Manager manager = new Manager(); // 创建Manager对象并为其赋值
manager.setName("李默");
manager.setSalary(3000);
manager.setDate(new Date());
manager.setBonus(2000);
// 输出经理和员工的属性值
System.out.println("员工的姓名:" + employee.getName());
System.out.println("员工的工资:" + employee.getSalary());
System.out.println("员工的入厂时间:" + employee.getDate());
System.out.println("经理的姓名:" + manager.getName());
System.out.println("经理的工资:" + manager.getSalary());
System.out.println("经理的入厂时间:" + manager.getDate());
System.out.println("经理的奖金:" + manager.getBonus());
}
}
结果如下:
员工的姓名:Lester
员工的工资:1800.0
员工的入厂时间:Wed Jan 09 17:03:49 CST 2019
经理的姓名:李默
经理的工资:3000.0
经理的入厂时间:Wed Jan 09 17:03:49 CST 2019
经理的奖金:2000.0
重写父类的方法
1.创建父类
新建项目OverrideMethod,并在其中创建一个Employee.java文件。在该类的主方法中创建getInfo()方法,输出我是员工的字符串:
public class Employee {
public String getInfo() { // 定义测试用的方法
return "父类:我是公司的员工!";
}
}
2.方法覆盖
在项目中再创建一个名为Manager的类,该类继承自Employee类,并重写了getInfo()方法,super继承父类方法:
public class Manager extends Employee {
@Override
public String getInfo() { // 重写测试用的方法
return "子类:我是公司的领导!";
}
public void getFaInfo(){
System.out.println(super.getInfo());
}
}
3.测试
创建一个名为OverrideMethod的测试类。在该类中分别创建Employee和Manager对象,并分别输出getInfo()方法的返回值
public class OverrideMethod {
public static void main(String[] args) {
Employee employee = new Employee(); // 创建Employee对象
System.out.println(employee.getInfo());
// 输出Employee对象的getInfo()方法返回值
Manager manager = new Manager(); // 创建Manager对象
System.out.println(manager.getInfo());// 输出Manager对象的getInfo()方法返回值
manager.getFaInfo();
}
}
运行,结果如下:
父类:我是公司的员工!
子类:我是公司的领导!
父类:我是公司的员工!