首页 » 编写高质量代码:改善Java程序的151个建议 » 编写高质量代码:改善Java程序的151个建议全文在线阅读

《编写高质量代码:改善Java程序的151个建议》建议148:增强类的可替换性

关灯直达底部

Java的三大特征:封装、继承、多态,这是每个初学者都会学习到的知识点,这里暂且不说封装和继承,单单说说多态。一个接口可以有多个实现方式,一个父类可以有多个子类,并且可以把不同的实现或子类赋给不同的接口或父类。多态的好处非常多,其中有一点就是增强了类的可替换性,但是单单一个多态特性,很难保证我们的类是完全可以替换的,幸好还有一个里氏替换原则来约束。

里氏替换原则是说“所有引用基类的地方必须能透明地使用其子类的对象”,通俗点讲,只要父类型能出现的地方子类型就可以出现,而且将父类型替换为子类型还不会产生任何错误或异常,使用者可能根本就不需要知道是父类型还是子类型。但是,反过来就不行了,有子类型出现的地方,父类型未必就能适应。

为了增强类的可替换性,就要求我们在设计类的时候考虑以下三点:

(1)子类型必须完全实现父类型的方法

子类型必须完全实现父类型的方法,难道还有能不实现父类型的方法?当然有,方法只是对象的行为,子类完全可以覆写,正常情况下覆写只会增强行为的能力,并不会“曲解”父类型的行为,一旦子类型的目的不是为了增强父类型行为,那替换的可能性就非常低了,比如这样的代码:


//枪

interface Gun{

//枪用来干什么的?射击杀戮!

public void shoot();

}

//手枪

class Handgun implements Gun{

@Override

public void shoot(){

System.out.println("手枪射击……");

}

}

//玩具枪

class ToyGun implements Gun{

@Override

public void shoot(){

//玩具枪不能射击,这个方法就不实现了

}

}


上面定义了两种类型的枪支:手枪和玩具枪,手枪可以用来射击敌人(shoot方法),但玩具枪就完全不同了,它不能用来射击,只是一个虚假的玩具而已,如果我们在要求使用枪支的地方传递了玩具枪会出现什么问题呢?代码如下:


public static void main(Stringargs){

Gun gun=new Handgun();

gun.shoot();

}


此处是一个手枪,用来射击,如果使用了子类型ToyGun,士兵将会拿着玩具枪来杀人,可是射不出子弹呀!如果在CS游戏中有这种事情发生,那就等着被人爆头,然后看着自己凄凉的倒地。

此处不能替换的原因是子类型没有完全实现父类型的方法,而是丢弃了父类型的行为能力,导致子类型不具备父类型的部分功能了。

(2)前置条件可以被放大

方法中的输入参数称为前置条件,它表达的含义是你要让我执行,就必须满足我的条件。前置条件是允许放大的,这样可以保证父类型行为逻辑的继承性,比如有这样的代码:


class Base{

public void doStuff(HashMap map){

}

}

class Sub extends Base{

public void doStuff(Map map){

}

}


这是Java的重载实现,子类型在实现父类型的同时也具备了自己的个性,可以处理比父类型更宽泛的任务,而且不会影响父类的任何行为,例如在如下代码中把父类型替换为子类型就不会有任何变化:


public static void main(Stringargs){

Base b=new Base();

b.doStuff(new HashMap());

}


此时,把Base全部替换为Sub,所有的行为全部还是由父类型Base实现的,子类型的doStuff方法并没有调用,也就是说,子类型可以在扩展前置条件的情况下保持类的可替换性。

(3)后置条件可以被缩小

父类型方法的返回值是类型T,子类同名方法(重载或覆写)的返回值为S,那么S可以是T的子集,这里又分为两种情况:

若是覆写,父类型和子类型的方法名名称就会相同,输入参数也相同(前置条件相同),只是返回值S是T类型的子集,子类型替换父类型完全没有问题。

若是重载,方法的输入参数类型或数量则不相同(前置条件不同),在使用子类型替换父类型的情况下,子类型的方法不会被调用到的,已经无关返回值类型了,此时子类依然具备可替换性。

增强类的可替换性,则增强了程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑,非常完美!