什么是继承

面向对象中,子类继承(Inherit)父类,避免重复的行为定义,不过并非为了避免重复定义行为就使用继承,滥用继承而导致程序维护上的问题。

如何正确判断使用继承的时机,以及继承之后如何活用多态,才是学习继承时的重点。

继承共同行为

继承基本上就是避免多个类间重复定义共同行为。

以实际的例子来说明比较清楚,假设你正在开发一款RPG(Role-playing game)游戏,一开始设定的角色有剑士与魔法师。

我们先来定义剑士

public class SwordsMan {
    private String name;
    private int level;
    private int blood;
    
    public int getBlood() {
        return blood;
    }

    public void setBlood(int blood) {
        this.blood = blood;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void fight() {
        System.out.println("拿剑攻击");
    }
  }

然后来定义魔法师

public class Magician {
    private String name;
    private int level;
    private int blood;
    
    public int getBlood() {
        return blood;
    }

    public void setBlood(int blood) {
        this.blood = blood;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    public void fight() {
        System.out.println("魔法攻击");
    }
    
    public void cure() {
        System.out.println("魔法治疗");
    }
}

我们来分析一下上面的两个类。

因为只要是游戏中的角色,都会具有角色名称、等级与血量,类中也都为名称、等级与血量定义了取值方法与设值方法。

因此SwordsMan和Magician中很多重复的代码。

举个例子来说,如果要将name、level、blood改为其他名称,那就要修改SwordsMan与Magician两个类,如果有更多类具有重复的程序代码,那就要修改更多类,造成维护上的不便。

如果要改进,就可以把相同的程序代码提升(Pull up)为父类

package base.inherit.game.v2;

public class Role {
    private String name;
    private int level;
    private int blood;
    
    public int getBlood() {
        return blood;
    }

    public void setBlood(int blood) {
        this.blood = blood;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

这个类在定义上没什么特别的新语法,只不过是将SwordsMan与Magician中重复的程序代码复制过来。

接着SwordsMan可以如下继承Role:

package base.inherit.game.v2;

public class SwordsMan extends Role {
	public void fight() {
		System.out.println("拿剑攻击");
	}
}

在这里看到了新的关键字extends,这表示SwordsMan会扩充Role的行为,也就是继承Role的行为,再扩充Role原本没有的fight()行为。

从程序面上来说,Role中有定义的程序代码,SwordsMan中都继承而拥有了,并定义了fight()方法的程序代码。

类似地,Magician也可以如下定义继承Role类:

package base.inherit.game.v2;

public class Magician extends Role {
	public void fight() {
		System.out.println("魔法攻击");
	}

	public void cure() {
		System.out.println("魔法治疗");
	}
}

最后我们来看一下 类图 。

image-20190511060434513

如何看出确实有继承了呢?从以下简单的程序可以看出:

package base.inherit.game.v2;

public class RPG {
    public static void showBlood(SwordsMan swordsMan) {
        System.out.printf("%s 血量 %d%n",
                swordsMan.getName(), swordsMan.getBlood());
    }

    public static void showBlood(Magician magician) {
        System.out.printf("%s 血量 %d%n",
                magician.getName(), magician.getBlood());
    }

    public static void main(String[] args) {
        SwordsMan swordsMan = new SwordsMan();
        swordsMan.setName("Justin");
        swordsMan.setLevel(1);
        swordsMan.setBlood(200);
        System.out.printf("剑士:(%s, %d, %d)%n", swordsMan.getName(),
                swordsMan.getLevel(), swordsMan.getBlood());

        Magician magician = new Magician();
        magician.setName("Monica");
        magician.setLevel(1);
        magician.setBlood(100);
        System.out.printf("魔法师:(%s, %d, %d)%n", magician.getName(),
                magician.getLevel(), magician.getBlood());
        
        showBlood(swordsMan);
        showBlood(magician);
    }
}

SwordsMan与Magician并没有定义getName()、getLevel()与getBlood()等方法,但从Role继承了这些方法,所以可以直接使用。

继承的好处之一,就是若你要将name、level、blood改为其他名称,那就只要修改Role.java就可以了,只要是继承Role的子类都无须修改。

上面代码中我们重载showBlood方法,分别来显示SwordsMan与Magician的血量。

发现还是㕛有重复的代码。而且我们假如有100个角色,我们难道要重载100个方法。

多态与is-a

在Java中,子类只能继承一个父类,继承除了可避免类间重复的行为定义外,还有个重要的关系,那就是子类与父类间会有is-a的关系。中文称为“是一种”的关系。

比如。SwordsMan继承了Role,所以SwordsMan是一种Role(SwordsMan is a Role)。

Magician继承了Role,所以Magician是一种Role(Magician is a Role)。

为何要知道继承时,父类与子类间会有“是一种”的关系?

因为要开始理解多态(Polymorphism),必须先知道你操作的对象是“哪一种”东西。

首先看一下下面的代码, 可以通过编译,没有问题。

SwordsMan swordsMan = new SwordsMan();
Magician magician = new Magician();

那下面的呢?

Role swordsMan = new SwordsMan();
Role magician = new Magician();

那那下面的呢?

SwordsMan swordsMan = new Role();
Magician magician = new Role();

编译器可以当做是语法检查器,要知道代码为何可以通过编译,为何无法通过编译,就是将自己当作编译程序,检查语法的逻辑是否正确,方式是从=号右边往左读:右边是不是一种左边呢(右边类是不是左边类的子类)。

image-20190511061705708

从右往左读,SwordsMan是不是一种Role呢?是的,所以编译通过。

Magician是不是一种Role呢?是的,所以编译通过。

同样的。

SwordsMan swordsMan = new Role(); // Role是不是一种SwordsMan
Magician magician = new Role(); // Role是不是一种Magician

明显的。Role不一定是一种SwordsMan,所以编译失败。

Role不一定是一种Magician,所以编译失败。

那么问题又来了。

Role role1 = new SwordsMan();
SwordsMan swordsMan = role1;

role1是一种SwordsMan呀? 还是报错怎么办?

我们可以使用强制转换,告诉编译器,我们自己是认为role1是一种SwordsMan。

Role role1 = new SwordsMan();
SwordsMan swordsMan = (SwordsMan)role1;

上面的代码类似于下图

image-20190511062349189

那下面的代码呢?

Role role2 = new Magician();
SwordsMan swordsMan = (SwordsMan)role1;

通过编译,但是执行的时候JVM会抛出java.lang.ClassCastException。

image-20190511062547564

所以我们要修改showBlood。

package base.inherit.game.v3;

public class RPG {

    public static void showBlood(Role role) {
        System.out.printf("%s 血量 %d%n",
        		role.getName(), role.getBlood());
    }

    public static void main(String[] args) {
        SwordsMan swordsMan = new SwordsMan();
        swordsMan.setName("Justin");
        swordsMan.setLevel(1);
        swordsMan.setBlood(200);
        System.out.printf("剑士:(%s, %d, %d)%n", swordsMan.getName(),
                swordsMan.getLevel(), swordsMan.getBlood());

        Magician magician = new Magician();
        magician.setName("Monica");
        magician.setLevel(1);
        magician.setBlood(100);
        System.out.printf("魔法师:(%s, %d, %d)%n", magician.getName(),
                magician.getLevel(), magician.getBlood());
        
        showBlood(swordsMan);
        showBlood(magician);
 
    }
}

在这里仅定义了一个showBlood()方法,参数声明为Role类型。

第一次调用showBlood()时传入了SwordsMan实例,这是合法的语法,因为SwordsMan是一种Role。

第一次调用showBlood()时传入了Magician实例也是可行,因为Magician是一种Role。

这样的写法好处为何?就算有100种角色,只要它们都是继承Role,都可以使用这个方法显示角色的血量,而不需要像前面重载的方式,为不同角色写100个方法,多态的写法显然具有更高的可维护性。

那么 什么叫多态呢 ?

就是使用单一接口操作多种类型的对象。若用以上的代码来理解,

在showBlood()方法中,既可以通过Role类型操作SwordsMan对象,也可以通过Role类型操作Magician对象。

重新定义行为

现在有个需求,请设计fight()方法,可以播放角色攻击动画。

我们在Role类添加如下方法

    public void fight() {
    	// 子类重新定义fight的实际行为
    }

我们来看一下如何使用

package base.inherit.game.v4;

public class RPG {

    public static void showBlood(Role role) {
        System.out.printf("%s 血量 %d%n",
        		role.getName(), role.getBlood());
    }
    
    public static void fight(Role role) {
    	 System.out.printf("播放角色攻击动画%n");
    	 role.fight();
    }

    public static void main(String[] args) {
        SwordsMan swordsMan = new SwordsMan();
        swordsMan.setName("Justin");
        swordsMan.setLevel(1);
        swordsMan.setBlood(200);
        System.out.printf("剑士:(%s, %d, %d)%n", swordsMan.getName(),
                swordsMan.getLevel(), swordsMan.getBlood());

        Magician magician = new Magician();
        magician.setName("Monica");
        magician.setLevel(1);
        magician.setBlood(100);
        System.out.printf("魔法师:(%s, %d, %d)%n", magician.getName(),
                magician.getLevel(), magician.getBlood());
        
        showBlood(swordsMan);
        showBlood(magician);

        fight(swordsMan);
        fight(magician);
    }
}

我们看一下子类

在继承父类之后,定义与父类中相同的方法,但执行内容不同,这称为重新定义(Override)。

因为对父类中已定义的方法执行不满意,所以在子类中重新定义执行。继承Role之后,也重新定义了fight()的行为。

Magician类

	public void fight() {
		System.out.println("魔法攻击");
	}

SwordsMan类

	public void fight() {
		System.out.println("拿剑攻击");
	}

RPG 类方法调用,到底是Role中定义的fight(),还是个别子类中定义的fight()呢?

如果传入fight()的是SwordsMan,role参数参考的就是SwordsMan实例,操作的就是SwordsMan上的方法定义。

image-20190511070111294

这就好比role牌子挂在SwordsMan实例身上,你要求有role牌子的对象攻击,发动攻击的对象就是SwordsMan实例

@Override注解。

为了防止重新定义的方法写错。我们可以在子类的方法上加上@Override。

抽象方法、抽象类

上边的例子。Role类的定义中,fight()方法区块中实际上没有撰写任何程序代码,虽然满足了多态需求,但会引发的问题是,你没有任何方式强迫或提示子类一定要重新定义fight()方法。

那我们应怎么才能强制子类去重新定义方法呢?

可以使用abstract标示该方法为抽象方法(Abstract method)。

类中若有方法没有定义,并且标示为abstract,表示这个类定义不完整,定义不完整的类就不能用来生成实例,这就好比设计图不完整,不能用来生产成品一样。

Java中规定内含抽象方法的类,一定要在class前标示abstract。

子类如果继承抽象类,对于抽象方法有两种做法,一种做法是继续标示该方法为abstract(该子类因此也是个抽象类,必须在class前标示abstract);另一种做法就是实现抽象方法。

我们使用抽象方法、抽象类来修改一下,之前的程序。

package base.inherit.game.v5;

public abstract class Role {
    private String name;
    private int level;
    private int blood;
    
    public int getBlood() {
        return blood;
    }

    public void setBlood(int blood) {
        this.blood = blood;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public abstract void fight();
}
调用父类的方法

如果子类里想要调用父类的方法,我们该怎么做呢?

答案是使用Super关键字。

Role类

    public String toString() {
        return String.format("(%s, %d, %d)", this.name, 
                this.level, this.blood);
    }

SwordsMan

    @Override
    public String toString() {
        return "剑士 " + super.toString();
    }

再看构造函数

如果类有继承关系,在创建子类实例后,会先进行父类定义的初始流程,再进行子类中定义的初始流程,也就是创建子类实例后,会先执行父类构造函数定义的流程,再执行子类构造函数定义的流程。

构造函数可以重载,父类中可重载多个构造函数,如果子类构造函数中没有指定执行父类中哪个构造函数,默认会调用父类中无参数构造函数。

根据上面game的代码演示 。

final关键字

class前也可以加上final关键字,如果class前使用了final关键字定义,那么表示这个类是最后一个了,不会再有子类,也就是不能被继承。

例子

String类

java.lang.Object

在Java中,子类只能继承一个父类,如果定义类时没有使用extends关键字指定继承任何类,那一定是继承java.lang.Object。