在继承中,凡是在父类已经实现的方法,其实算是一种契约或者规范,子类不应该在进行更改(重写);但是,由于这一点不是强制要求,所以当子类进行重写的时候,就会对继承体系产生破坏。 同时,继承带来便利的时候,也有弊端:给程序带来了侵入性,增加了对象之间的耦合性,可移植性低。当你修改基类时,子类都需要进行相应的修改。 那么,如何能够保持继承的优点,同时减少缺点对程序的影响呢?也就是我们要讨论的主角——“里氏替换原则”。
如果对每一个类型为S的对象o1, 都用类型为T的对象o2, 使得以T定义的所有程序P在所有的对象o1都替换为o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
第二种类型通俗易懂,就是说, 只要父类出现的地方,都应该能用其子类进行替换。 但是反过来却不一定成立,也就是子类所在的地方替换成父类,是不一定成立的。
根据定义,我们总结出以下几点:
结合代码理解一下:
public abstract class SuperClass { public abstract void sayHi(); public void doSomething() { System.out.println("父类被执行..."); } //定义一个加法运算函数 public int add(int i, int j) { int result = i + j; System.out.println("result = " + result); return result; } } public class SubClass extends SuperClass { @Override public void sayHi() { System.out.println("子类重写了父类的sayHi方法..."); } //子类特有的方法 public void selfMethod() { System.out.println("子类特有的方法"); } //子类重写了父类的非抽象加法运算方法 @Override public int add(int i, int j) { int result = i - j; System.out.println("result = " + result); return result; } } public class Client { public static void main(String[] args) { SuperClass clazz = new SubClass(); clazz.doSomething(); SubClass subClass = new SubClass(); //子类调用自己的方法 subClass.selfMethod(); SuperClass superClass = (SuperClass) subClass; superClass.add(11,22); } } //运行结果如下: 父类被执行... 子类特有的方法 result = -11 复制代码
当你继承一个基类时,编译器会要求你来实现基类的抽象方法,否则,会报错。 但是,对于已经实现的方法,编译器便不会强制让你去重写(也不推荐这样做):在上面的demo中,子类重写了父类的add方法,在调用时,出现了错误(基类定义的加法逻辑,被子类重写为了一个减法)。这样子,在父类出现的地方,不能由子类完全替换,违背了“里氏替换原则”。
在上面的demo中,子类定义了特有的方法selfMethod(),可以实现其他的业务逻辑。
//子类特有的方法 public void selfMethod() { System.out.println("子类特有的方法"); } 复制代码
下来看一下demo:
public class Father { public Collection doSomething(HashMap map) { System.out.println("父类被执行..."); return map.values(); } } public class Son extends Father { public Collection doSomething(Map map) { System.out.println("子类被执行..."); return map.values(); } } public class Client1 { public static void main(String[] args) { invoke(); } public static void invoke() { // Father clazz = new Father(); Son clazz = new Son(); HashMap hashMap = new HashMap(); clazz.doSothing(hashMap); } //运行结果:父类执行... 复制代码
在demo中,基类定义了一个doSomething方法,接受的形参类型为HashMap,子类 重载 了基类的这个方法,并且将接受的形参类型扩展为Map。但是,我们发现,在父类出现的地方,替换为子类后,运行的结果不变。也就是说,子类对象在方法执行时被替换为父类对象,重载的方法并未被执行。如果想要执行子类的方法,就必须重写,这是正确的。 如果反过来呢?下面通过反证来证明第三点: 当子类的方法中的前置条件比父类小的时候,情况会怎么样呢? 在父类和子类中重新定义这两个方法,符合子类的前置条件小于父类这一条件即可。
//修改父类的方法,前置条件扩大为Map public Collection doSomething(Map map) { System.out.println("父类被执行..."); return map.values(); } //修改子类,前置条件缩小为HashMap public Collection doSomething(HashMap map) { System.out.println("子类被执行..."); return map.values(); } 复制代码
当客户端的invoke()方法中,通过Father类型调用doSomething方法时,运行结果如下: //运行结果: 父类执行****
将对象换成Son类型时,执行结果时:
public static void invoke() { // Father clazz = new Father(); Son clazz = new Son(); HashMap hashMap = new HashMap(); clazz.doSothing(hashMap); } //执行结果: 子类被执行... 复制代码
我们看到了,子类并没有重写父类的方法,但是子类的方法被执行了(这是因为调用方法时,优先选择参数类型一致的方法,也就是重载方法,找不到的话才去找类型为形参的基类的方法),但是,这个现象在逻辑上面是错误的。