继 承
继承是Java中最基础也是重要的特性之一,Java中使用extends关键字来实现继承,并且Java只支持单继承。
继承机制优点:
1.代码共享,减少创建类的工作量。
2.提高代码重用性
3.子类沿袭父类,又可以异于父类
4.提高代码可扩展性
5.提高产品或项目的开放性
继承机制缺点:
1.继承具有侵入性(一旦使用继承,就必须拥有父类所有的属性和方法)
2.降低代码灵活性(子类拥有父类的属性和方法,约束了子类的行为)
3.增强耦合性(父类方法、变量或常量发生变化,则需要考虑到其子类的重构)
里氏替换原则
总的来说继承机制是利大于弊的。但为了突出其“利”的一面,减少其“弊”的一面,故引入了里氏替换原则(Liskov Substitution Principle,LSP)。它有以下两种定义:
1.If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substisuted for o2 then S is a subtype of T.
2.Functions that use pointers or references to base classes must be able to use objects of derived classes without konwing it.
通俗的说,即父类出现的地方子类也可以出现,并且将父类用子类替换后,也不会产生任何问题。然鹅,需要注意的是里氏替换原则反过来使用是不行的,子类出现的地方,父类未必适用。
里氏替换原则为良好的继承定义了一个规范,一句简单的话包含了4层含义。
1.子类必须完全实现父类的方法
在类中调用其它类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计以及违背了LSP原则。
如果子类不能完整的实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
2.子类可以拥有自己的“个性”
子类可以拥有自己的方法和属性,也可以重写(Override)或重载(Overload)父类的方法。从而拥有自己的“个性”。但这里提起是为了再次说明一个问题——里氏替换原则可以正着用,但不能反过来使用,即子类出现的地方,父类未必可以胜任。
3.覆盖或实现父类方法时输入参数可以被放大
父类的一个方法的形参是一个类型T,子类的相同方法(重载或重写)的形参是S,那么里氏替换原则就要求S必须大于等于T。也就是说,要么S和T是同一类型,要么S是T的父类。
为了便于理解,这里通过代码进行说明。注意此处父类型doSomething方法参数HashMap范围小于子类型doSomething方法参数Map。
父类——Father01
import java.util.Collection;
import java.util.HashMap;
//父类
public class Father01 {
public Collection doSomething(HashMap map) {
System.out.println("父类被执行...");
return map.values();
}
}
子类——Son01
import java.util.Collection;
import java.util.Map;
//子类
public class Son01 extends Father01 {
//需要注意的是,这里并不是对父类方法的重写,而是重载。使用Override注解会报错
public Collection doSomething(Map map) {
System.out.println("子类被执行...");
return map.values();
}
}
场景类——Client01
import java.util.HashMap;
//场景类
public class Client01 {
public static void invoker() {
Father01 f = new Father01();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
输出
根据里氏替换原则,父类出现的地方可以用子类替换。那么我们将场景类做出相应更改。
场景类——Client02
import java.util.HashMap;
//场景类
public class Client02 {
public static void invoker() {
//子类替换父类
Son01 s = new Son01();
HashMap map = new HashMap();
s.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
输出
可以看到,这里用子类替换后,即使子类中存在同名的重载方法,程序仍然能正确的执行父类方法。
我们继续尝试对换父、子类中方法的参数,使得父类方法的形参范围大于子类方法的形参。
父类——Father02
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
//父类
public class Father02 {
public Collection doSomething(Map map) {
System.out.println("父类被执行...");
return map.values();
}
}
子类——Son02
import java.util.Collection;
import java.util.HashMap;
//子类
public class Son02 extends Father02 {
public Collection doSomething(HashMap map) {
System.out.println("子类被执行...");
return map.values();
}
}
场景类——Client03
import java.util.HashMap;
//场景类
public class Client03 {
public static void invoker() {
Son02 s = new Son02();
HashMap map = new HashMap();
s.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
输出
可以看到,当子类重载或覆盖的方法的输入参数小于父类方法的输入参数时,会有不符合预期的情况出现,同样违反了里氏替换原则中父类出现的地方子类也可以出现这一思想。使得程序不易维护,且出现问题也较难以排查
4.覆盖或实现父类方法时输出结果可以被缩小
父类的一个方法返回值是一个类型T,子类的相同方法(重载或重写)的返回值是S,那么里氏替换原则就要求S必须小于等于T。也就是说,要么S和T是同一类型,要么S是T的子类
这点较好理解,若子类重载或重写父类方法时返回值的范围大于父类,比如父类方法返回HashMap类,而重写或重载父类方法的子类方法返回Map类型。那么在场景类中将父类用子类替换时,就可能会出现问题,进而影响程序执行。
写在最后
采用里氏替换原则的目的是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的父类仍然可以运行。
在实际使用中,采用里氏替换原则应当尽量避免子类的“个性”,一旦子类产生“个性”,这个子类和父类之间的关系将难以调和。
P.s. 本文是我学习秦小波先生《设计模式之禅》一书的过程中,结合一点个人的想法写下的学习笔记。若有遗漏,敬请各位指正。也希望能找寻更多志同道合的朋友,一起分享交流。