严格的定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。
通俗的定义:所有引用基类的地方必须能透明地使用其子类的对象。
更通俗的定义:子类可以扩展父类的功能,但不能改变父类原有的功能(一方面指不改变父类原功能的逻辑,另外一方面也指不改变该方法的含义)。
对于基类中定义的所有子程序,用在它的任何一个派生类中时的含义都应该是相同的。这样继承才不会增加复杂度,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。如果我们必须要不断地思考不同派生类的实现在语义上的差异,继承就只会增加复杂度了。
有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
里氏替换原则的关键点在于不能覆盖父类的非抽象方法。父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
public class TestFirst {
public static void main(String args[]){
Operator operator = new Operator();
System.out.println("1 + 2 = " + operator.add(1 , 2));
operator = new ChildOperator();
System.out.println("1 + 2 = " + operator.add(1 , 2));
}
static class Operator{
public int add(int a, int b){
return a + b;
}
}
static class ChildOperator extends Operator{
public int add(int a, int b){
return a - b;
}
}
}
输出
1 + 2 = 3
1 + 2 = -1
上面的例子有两个问题:
- 问题1:改变了父类原有功能的逻辑
子类改变了父类的逻辑,导致输出的结果不对,假设operator是一个全局对象,在很多出地方使用,一直都是采用了1 + 2 = 3这种逻辑,后来产品需求改变了,需要一种新的模式是1 + 2 = -1这种,但是在很多外面地方需要采取以前1+2=3的逻辑的地方却产生了1+2=-1,这就出现了问题。
原有父类的add方法的含义是两个数字相加,但是子类的逻辑是两个数字相减,导致函数的含义发生改变,然而外界对这个函数的理解往往以为是前者,导致代码功能出现问题。
在继承父类属性和方法的同时,每个子类也都可以有自己的个性,在父类的基础上扩展自己的功能。前面其实已经提到,当功能扩展时,子类尽量不要重写父类的方法,而是另写一个方法,所以对上面的代码加以更改,使其符合里氏替换原则,代码如下
static class ChildOperator2 extends Operator{
public int subtract(int a, int b){
return a - b;
}
}
如果继承为了不是拓展,那将毫无意义,上面例子中子类拓展了减法功能
个人的理解是,父亲能做的,子类应该也能完成,即向上兼容,所以子类的参数范围应该不小于父类,所以重写的时候相同方法应该参数范围大于父类,当然就算是小于父类,代码上依然能正常执行,但是对于继承就感觉不太好了,子类变得越来越弱了。
static class Food{
public String getFood(){
return "unkown food";
}
}
static class MeatFood extends Food{
@Override
public String getFood() {
return "meat";
}
}
static class VegetablesFood extends Food{
@Override
public String getFood() {
return "vegetables";
}
}
static class Dog{
public void eat(MeatFood food) {
System.out.println("dog eat " + food.getFood());
}
}
static class Hashiqi extends Dog{
public void eat(Food food) {//放大范围
System.out.println("hashiqi eat " + food.getFood());
}
}
public static void main(String args[]){
Hashiqi animal = new Hashiqi();
animal.eat(new MeatFood());
animal.eat(new VegetablesFood());
}
输出
dog eat meat//调用的是父类方法
hashiqi eat vegetables//调用的是子类方法
当然如果改成下面:
static class Hashiqi extends Dog{
public void eat(FishFood food) {//缩小范围
System.out.println("hashiqi eat " + food.getFood());
}
}
public static void main(String args[]){
Hashiqi animal = new Hashiqi();
animal.eat(new MeatFood());
animal.eat(new FishFood());
}
输出
dog eat meat
hashiqi eat fish
虽然也是正常执行,但是同样的eat方法,子类的方法能吃的确更少了,这就是典型的“挑食”,感觉不好
举例如下:
interface Tool{
String getTool();
}
static class Leg implements Tool{
@Override
public String getTool() {
return "legs";
}
@Override
public String toString() {
return getTool();
}
}
static class FourLeg extends Leg{
@Override
public String getTool() {
return "four legs";
}
}
static class Wing implements Tool{
@Override
public String getTool() {
return "wings";
}
@Override
public String toString() {
return getTool();
}
}
static class Animal{
public Leg walk(){
return new Leg();
}
}
static class Dog extends Animal{
@Override
public FourLeg walk(){
return new FourLeg();
}
public Tool walk(int i){
return new Wing();
}
}
public static void main(String args[]){
Dog dog = new Dog();
System.out.println("dog move with " + dog.walk());
System.out.println("dog move with " + dog.walk(0));
}
输出
dog move with four legs
dog move with wings//很奇怪啊,狗有翅膀
上面例子中,重写父类方法,如Dog.walk()方法,返回值是可以比父类小(当然,如果返回值比父类大,编译器一般直接会报错了),当然重载的时候不会报错,如Dog.walk(0),但是返回值范围变大了,当用户调用这个方法的时候居然发现狗有翅膀,就显得很不合逻辑了。比如某个全局变量,一直调用A方法,返回一种类型B,后来A方法被重载了,新方法返回了C类型,但客户端(未必知道)去调用的时候,本来想着还是返回B,却发现返回了C,就很尴尬了。