里氏替换原则主要是发生在父类和子类之间,说到父类和子类,在面向对象的语言中, 继承是必不可少的、 非常优秀的语言机制, 它有如下优点:
- 代码共享,减少创建类的代码量,每一个子类可以拥有父类的方法和属性。
- 提高代码的重用性。
- 子类可以形似父类,但又异于父类。
- 提高代码的扩展性。
缺点
- 继承是侵入性的。 只要继承, 就必须拥有父类的所有属性和方法;
- 降低代码的灵活性。 子类必须拥有父类的属性和方法, 让子类自由的世界中多了些约
束; - 增强了耦合性。 当父类的常量、 变量和方法被修改时, 需要考虑子类的修改, 而且在
缺乏规范的环境下, 这种修改可能带来非常糟糕的结果——大段的代码需要重复。
目录
定义
里氏替换法则有两种定义:
- 如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
- 所有引用基类(父类)的地方必须能透明地使用其子类的对象。
第二个定义是最清晰明确的,通俗点讲只要父类能出现的地方我子类就可以出现,而且调用子类还不产生任何的错误或异常,调用者可能根本就不需要知道是父类还是子类。但是反过来就不成了,有子类出现的地方,父类未必就能适应。
含义
里氏替换原则有4层含义。
- 子类必须完全实现父类的方法。
- 子类可以有自己的个性。
- 覆盖或实现父类的方法时输入参数可以被放大。
- 覆写或实现父类的方法时输出结果可以被缩小。
1.子类必须完全实现父类的方法。
子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法.
//父类
public class Father{
public void add(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
//子类
public class Son extends Father{
@Override
public void add(int a,int b){
System.out.println(a+"-"+b+"="+(a-b));
}
}
public static void main(String[] args){
System.out.println("父类的运行结果");
Father father=new Father();
father.add(1,2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.println("子类替代父类后的运行结果");
Son son=new Son ();
son.add(1,2);
}
运行结果: 父类的运行结果 1+2=3 子类替代父类后的运行结果 1-2=-1
我们定义的这个add方法是加数,而不是减数,子类破坏了父类的加数方法。
2.子类可以有自己的个性
子类中可以增加自己特有的方法。
//父类
public class Father{
public void add(int a,int b){
System.out.println("父类:"+a+"+"+b+"="+(a+b));
}
}
//子类
public class Son extends Father{
public void reduce(int a,int b){
System.out.println("子类:"+a+"-"+b+"="+(a-b));
}
}
public static void main(String[] args){
Father father=new Father();
father.add(1,2);
Son son=new Son ();
son.add(2,3);
son.reduce(2,1);
}
运行结果:
父类:1+2=3
父类:2+3=5
子类:2-1=1
3. 覆盖或实现父类的方法时输入参数可以被放大。
当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
public class Father {
public Collection doSomething(HashMap map){
System.out.println("父类被执行...");
return map.values();}
}
public class Son extends Father {
//放大输入参数类型,与父类的方法名相同, 但又不是覆写(Override) 父类的方法。 你加
//个@Override试试看, 会报错的, 为什么呢? 方法名虽然相同, 但方法的输入参数不同, 就
//不是覆写,
public Collection doSomething(Map map){
System.out.println("子类被执行...");
return map.values();
}
}
我们来调用一下Father 和Son 类的doSomething方法
public static void main(String[] args) {
//父类存在的地方, 子类就应该能够存在
Father f = new Father();
Son son = new Son();
HashMap map = new HashMap();
f.doSomething(map);
son.doSomething(map);
}
运行结果:
父类被执行了
父类被执行了
运行结果两个是一样的, 看明白是怎么回事了吗? 父类方法的输入参数是HashMap类型, 子类的输入参数是Map类型, 也就是说子类的前置条件(输入参数类型)的范围扩大了, 子类代替父类传递到调用者中, 子类的方法永远都不会被执行。 这是正确的。
如果我们把这个前置条件反过来,看下边代码。
public class Father {
public Collection doSomething(Map map){
System.out.println("父类被执行...");
return map.values();}
}
public class Son extends Father {
//放大输入参数类型,与父类的方法名相同, 但又不是覆写(Override) 父类的方法。 你加
//个@Override试试看, 会报错的, 为什么呢? 方法名虽然相同, 但方法的输入参数不同, 就
//不是覆写,
public Collection doSomething(HashMap map){
System.out.println("子类被执行...");
return map.values();
}
}
public static void main(String[] args) {
//父类存在的地方, 子类就应该能够存在
Father f = new Father();
Son son = new Son();
HashMap map = new HashMap();
f.doSomething(map);
son.doSomething(map);
}
运行结果:
父类被执行了
子类被执行了
运行结果不一样的, 看明白是怎么回事了吗? 父类方法的输入参数是Map类型, 子类的输入参数是HashMap类型, 也就是说子类的前置条件(输入参数类型)的范围缩小了, 子类代替父类传递到调用者中, 父类的方法永远都不会被执行。 这是错误的,(主要第一条不得重写(覆盖)父类的非抽象(已实现)方法),明白了没啊。
4.覆写或实现父类的方法时输出结果可以被缩小
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
abstract class Father {
public abstract Map getMap();
}
class Son extends Father{
@Override
public HashMap getMap(){
HashMap b=new HashMap();
b.put("b","子类被执行...");
return b;
}
}
public static void main(String[] args){
Father father=new Father();
System.out.println(father.getMap());
}
注意:HashMap是Map的子类,
父类的一个方法的返回值是一个类型T(Map), 子类的相同方法(覆写) 的返回值为S(HashMap), 那么里氏替换原则就要求S(HashMap)必须小于等于T(Map), 上边的案例完全满足这个条件。
如果我们要是相反,有什么结果?
程序直接就报错了,什么报错信息了?
报错信息
英文:'getMap()' in 'com.fry.mylibrary.oaid.MiitHelper.Son' clashes with 'getMap()' in 'com.fry.mylibrary.oaid.MiitHelper.Father'; attempting to use incompatible return type
中文:'com.fry.mylibrary.oaid.MiitHelper.Son' 中的 'getMap()' 与 'com.fry.mylibrary.oaid.MiitHelper.Father' 中的 'getMap()' 冲突; 尝试使用不兼容的返回类型
在里氏替换原则 要么S和T是同一个类型, 要么S是T的子类, 为什么呢? 分两种情况, 如果是覆写, 父类和子类的同名方法的输入参数是相同的, 两个方法的范围值S小于等于T, 这是覆写的要求, 这才是重中之重, 子类覆写父类的方法, 天经地义。 如果是重载, 则要求方法的输入参数类型或数量不相同, 在里氏替换原则要求下, 就是子类的输入参数宽于或等于父类的输入参数, 也就是说你写的这个方法是不会被调用的, 参考上面讲的前置条件。
最佳实践
在项目中, 采用里氏替换原则时, 尽量避免子类的“个性”, 一旦子类有“个性”, 这个子
类和父类之间的关系就很难调和了, 把子类当做父类使用, 子类的“个性”被抹杀——委屈了
点; 把子类单独作为一个业务来使用, 则会让代码间的耦合关系变得扑朔迷离——缺乏类替
换的标准。
参考书籍:设计模式之禅。