学习随笔

C,Java,Python,Html代码重复意味着有改进空间

继承与多态

面向对象中,子类继承( Inherit)父类,避免重复的行为定义,不过并非为了避免重复定义行为就使用继承,滥用继承而导致程序维护上的问题时有所闻。如何正确判断使用继承的时机,以及继承之后如何活用多态,才是学习继承时的重点。
1、继承共同行为
继承基本上就是避免多个类间重复定义共同行为。以实际的例子来说明比较清楚,假设你正在开发一款RPG(Role- Playing Game)游戏,一开始设定的角色有剑士与魔法师。

//剑士类
package hello;
import static java.lang.System.*;

public class SwordsMan {
    private String name;
    private int level;
    private int blood;

    public void fight() {
        out.println("普通攻击");
    }
    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;
    }

}
//魔法师类
package hello;
import static java.lang.System.*;

public class Magician {
    private String name;
    private int level;
    private int blood;

    public void fight() {
        out.println("魔法攻击");
    }
    public void cure() {
        out.println("魔法治疗");
    }
    ......//相同
}

只要是游戏中也都为名称、等级与血量定义了取值方法中相对应的程序代码重复了。要将name、level、blood改为其他名称,那就要修改很多类,造成维护上的不便。如果要改进,就可以把相同的程序代码提升( Pull Up)为父类

package hello;

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;
    }
}

子类继承:

//剑士
package hello;
import static java.lang.System.*;

public class SwordsMan extends Role {
    public void fight() {
        out.println("挥剑攻击");
    }
}
//魔法师
package hello;
import static java.lang.System.*;

public class Magician extends Role{

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

}

extends继承父类,Role中有定义的程序代码子类中都继承并且拥有,也可以定义fight()等其他方法。如:

package hello;
import static java.lang.System.*; 

public class RPGgame {
    public static void main(String[] args) {
        demoSwordsMan();
        demoMagician();
    }

    private static void demoSwordsMan() {
        SwordsMan swordsman=new SwordsMan();
        swordsman.setName("李白");
        swordsman.setLevel(1);
        swordsman.setBlood(100);
        out.printf("剑士:(%s,%d,%d)%n",swordsman.getName(),swordsman.getLevel(),swordsman.getBlood());
    }

    private static void demoMagician() {
        Magician magician=new Magician();
        magician.setName("安琪拉");
        magician.setLevel(1);
        magician.setBlood(100);
        out.printf("魔法师:(%s,%d,%d)%n",magician.getName(),magician.getLevel(),magician.getBlood());
    }

}

继承的好处之一就是 修改name、level、blood直接改Role.java.
private成员也可以被继承,如上面name、blood、level,只不过子类无法直接存取,必须通过父类提供的方法来存取。
2、多态与is-a
在Java中,子类只能继承一个父类,继承除了可避免类间重复的行为定义外,还有个重要的关系,那就是子类与父类间会有is-a的关系,中文称为“是一种”的关系,这是什么意思?以前面范例来说, Swordsman继承了Role,所以 Swordsman是一种Role( Swordsman Is a Ro1e), Magician继承了Role,所以 Magician是一种 Role(Magician is a Role)
为何要知道继承时,父类与子类间会有“是一种”的关系?因为要开始理解多态( Polymorphism),必须先知道你操作的对象是“哪一种”东西。
来看实际的例子:

SwordsMan swordsman=new Swordsman();
Magician magician=new Magician();

Role r1=new SwordsMan();
Role r2=new Magician();

上面的可以通过编译
编译程序就是语法检查器,要知道以上程序片段为何可以通过编译,为何无法通过编译,就是将自己当作编译程序,检查语法的逻辑是否正确,方式是从=号右边往左读:右边是不是一种左边呢(右边类是不是左边类的子类)。
从右往左读, Swordsman是不是一种Role呢?是的,所以编译通过。 Magician是不是一种Role呢?是的,所以编译通过。同样的判断方式,可以知道为何以下编译失败:

Swordsman swordsman=new Role();
Magician magician=new Role();

Role是不是一种 Magician?编译程序认为第一行Role不一定是一种
Swordsman。所以编译失败;对于第二行,编译程序认为Role不一定是一种 Magician,所以编译失败。继续把自己当成编译程序,再来看看以下的程序片段是否可以通过编译:

Role role= new Swordsman();
Swordsman swordsman =role;

这个程序片段最后会编译失败,先从第一行看, Swordsman是一种Role,所以这行可以通过编译。编译程序检查这类语法,一次只看一行,就第二行而言,编译程序看到role1为Role声明的名称,于是检查Role是不是Swordsman,答案是不一定,所以编译失败在第二行失败。
编译程序会检查父子类间的“是一种”关系,如果你不想要编译程序啰唆,可以叫它住嘴 Role role= new Swordsman();
Swordsman swordsman =(SwordsMan)role;

对于第二行,原本编译程序想告诉你
Role不一定是一种 Swordsman,,但你加上 (Swordsman)
让它住嘴了,因为这表示,你就是要让Role强制扮演Swordsman,既然你都明确要求编译程序别啰唆了,编译程序就让这段程序代码通过编译了,不过后果得自行负责。

Role role= new Magician();
Swordsman swordsman =(SwordsMan)role;

以上的程序片段,编译可以成功,但执行时期会出错。对于第一行, Magician是一种Role,可以通过编译,对于第二行,role2为Role类型编译程序原本认定Role不一定是一种 Swordsman而想要啰唆,但是你明确告诉编译程序就是要让Role扮演为 Swordsman,所以编译程序就让你通过编译了,不过后果自负。实际上,role2参考的是 Magician,你要让魔法师假扮为剑士,这在执行上会是个错误,JVM会抛出java. lang.Classcastexception使用是一种(is-a)原则,就可以判断何时编译成功,何时编译失败,以及将扮演(Cast)看作叫编译程序住嘴语法,并留意参考的对象实际类型,就可以判断何时扮演成功,何时会抛出 Classcastexception。以上这些看似在玩弄语法其实涉及到程序又没有弹性好不好维护的问题 如果请你设计static方法显示所有角色:

private static void showRole(Magician magician) {
        out.printf("魔法师:(%s,%d,%d)%n",magician.getName(),magician.getLevel(),magician.getBlood());
    }
    private static void showRole(SwordsMan swordsman) {
        out.printf("剑士:(%s,%d,%d)%n",swordsman.getName(),swordsman.getLevel(),swordsman.getBlood());
    }

只要这样调用:

        SwordsMan swordsman=new SwordsMan();
        Magician magician=new Magician();
        showRole(magician);
        showRole(swordsman);

但是如果有很多很多角色呢?如果这些角色都继承自Role就可以:

private static void showRole(Role role) {
        out.printf("魔法师:(%s,%d,%d)%n",role.getName(),role.getLevel(),role.getBlood());
    }
        SwordsMan swordsman=new SwordsMan();
        Magician magician=new Magician();
        showRole(magician);//magician是一种Role
        showRole(swordsman);//swordsman是一种Role

这就是多态的写法即:用单一接口操作多种类型的对象。就像上面可以用Role类型操Magician、Swordsman对象(这里的接口不专指Java中的interface,指对象上的可操作方法)。
3、重新定义行为
如果设计static方法播放角色攻击动画:

private static void drawFight(Role role) {
        role.fight;
    }

Role中并没有fight方法,只是Magician、Swordsman对象有这种方法,所以我们可以在父类定义fight方法:

package hello;

public class Role {
......
    public void fight() {
        //空方法,子类重新定义
    }
}
//之后下面的方法就可以执行了如:drawFight(Magician);
private static void drawFight(Role role) {
        role.fight;
    }

子类重新定义fight方法:

public class SwordsMan extends Role {
    @Override//检查是否重新定义
    public void fight() {
        out.println("挥剑攻击");
    }

虽然Role可以代表SwordsMan 、Magician但是实际上你传入的是哪个实例就会参考至哪个实例重新定义的方法:drawFight(Magician);drawFight(SwordsMan ); 执行各自重新定义的方法。

细节注意
有时候重新定义父类中方法时并不是完全不满意,只是想在其前后加点东西如:

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

如果在 Swordsman子类中重新定义 toString()的内容时,可以执行Role中的
方法取得字符串结果,再连接“剑士”字样。在Java中,如果 toString ()想取得父类中的方法定义,可以在调用方法前,加上 super关键字。例如:

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

如果使用 super关键字调用的父类方法,不能定义为 private(因为这就限定只能在类内使用)。
重新定义方法要注意,对于父类中的方法权限,只能扩大但不能缩小。若原来成员 public,子类中重新定义时不可为 private或 protected。
在JDK5后重新定义方法时如果返回类型是父类中返回类型的子类中也可以通过编译。
static方法属于类拥有,如果子类定义相同签署的static成员,该成员属于子类所有并非重新定义,static方法没有多态,因为对象不会个别拥有static成员
4、再看构造函数
如果类有继承关系,在创建子类实例后,会先进行父类定义的初始流程,再进行子类中定义的初始流程,也就是创建子类实例后,会先执行父类构造函数定义的流程,再执行子类构造函数定义的流程
构造函数可以重载,父类中可重载多个构造函数,如果子类构造函数中没有指定执行父类中哪个构造函数,默认会调用父类中无参数构造函数。如果这样撰写程序:

package hello;

class Hello {
    Hello(){
        System.out.println("some");
    }
}
class Other extends Hello {
    Other(){
        System.out.println("other");
    }
}

如果尝试new Other();会得到some other因为如果子类构造函数中没有指定执行父类中哪个构造函数,默认会调用父类中无参数构造函数。上面的其实就是

package hello;

class Hello {
    Hello(){
        System.out.println("some");
    }
}
class Other extends Hello {
    Other(){
        super();//先执行父类中无参数的构造函数
        System.out.println("other");
    }
}

如果定义有参数构造函数,编译程序就不会加入默认构造函数,如:

package hello;

class Hello {
    Hello(int i){
        System.out.println("some");
    }
}
class Other extends Hello {
    Other(){//此处出错
        System.out.println("other");
    }
}

Other的构造函数没有指定调用父类哪个构造函数,默认构造函数也没有。为了日后使用上的弹性,建议先加入无参构造函数。

阅读更多
版权声明:博主原创或改进文章 https://blog.csdn.net/zkd758/article/details/79950187
文章标签: Java
个人分类: Java
上一篇Eclipse快捷键
下一篇java.lang.Object(数组收集对象&重新定义)
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭