6大设计原则2:里氏替换原则

里氏替换原则:LSP

定义:

如果对于每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都换为o2时,程序的行为没有发生变化,那么S是T的子类型。

在继承的时候,父类出现的地方子类就可以出现,子类可替代父类,因为子类中有父类的方法,然而父类却不可以替代子类,因为子类中可能有父类没有的方法。这就是所谓的向下转型是不安全的。

使用继承有很多优点,可以提高代码的重用性,提高可扩展性、开放性,但是不可否认,继承也是有缺点的:

1.继承是侵入性的,只要继承,就必须拥有父类的所有属性和方法;

2.降低代码的灵活性

3.增强了耦合性。

解决方案就是里氏替换原则。

4个含义:

1.子类必须完全实现父类的方法

2.子类可以有自己的方法

3.覆盖或实现父类的方法时,输入参数可以被放大

4.覆写或实现父类的方法时,输出结果可以被缩小

前两个含义比较好理解,这里就不再赘述,主要说一下3和4。

先说第3个,覆盖或实现父类的方法时,输入参数可以被放大。

先看一个例子:

class Father {
	public Collection dosomething(HashMap map) {
		System.out.println("父类被执行--->");
		return map.values();
	}
}

class Son extends Father {
	public Collection dosomething(Map map) {
		System.out.println("子类被执行--->");
		return map.values();
	}
}

public class Client {
	public static void main(String[] args) {
		// 父类存在的地方就可以替换为子类
		Father f = new Father();
		HashMap map = new HashMap();
		f.dosomething(map);
	}
}


代码运行结果是:

父类被执行--->

根据里氏替换原则,将父类改为子类:

public class Client {
	public static void main(String[] args) {
		// 父类存在的地方就可以替换为子类
		// Father f = new Father();
		Son f = new Son();
		HashMap map = new HashMap();
		f.dosomething(map);
	}
}


然而输出结果还是父类被执行。。。

父类方法的参数是HashMap,而子类方法的参数是Map,也就是说子类的参数类型范围大,子类代替父类传递到调用者中,子类的方法永远不会被执行。如果想要执行子类中的方法的话就需要覆写父类中的方法,覆写就是父类中的方法一模一样的出现在子类中。

class Father {
	public Collection dosomething(HashMap map) {
		System.out.println("父类被执行--->");
		return map.values();
	}
}

class Son extends Father {
	// public void dosomething(Map map) {
	// System.out.println("子类被执行--->");
	// }
	@Override
	public Collection dosomething(HashMap map) {
		// TODO Auto-generated method stub
		System.out.println("子类被执行--->");
		return map.values();
	}
}

public class Client {
	public static void main(String[] args) {
		// 父类存在的地方就可以替换为子类
		// Father f = new Father();
		Son f = new Son();
		HashMap map = new HashMap();
		f.dosomething(map);
	}
}


这是正常的。

如果父类参数的参数类型范围大于子类输入参数类型的话,会出现什么问题呢?会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入子类的方法范畴。

修改一下上面的代码,扩大父类参数范围,缩小子类参数范围。

class Father {
	public Collection dosomething(Map map) {
		System.out.println("父类被执行--->");
		return map.values();
	}
}

class Son extends Father {
	public Collection dosomething(HashMap map) {
		System.out.println("子类被执行--->");
		return map.values();
	}

}

public class Client {
	public static void main(String[] args) {
		// 父类存在的地方就可以替换为子类
		Father f = new Father();
		Son f1 = new Son();
		HashMap map = new HashMap();
		f.dosomething(map);
		f1.dosomething(map);
	}
}

f执行父类的方法,f1执行子类的方法。

这就不正常了

子类在没有覆写父类方法的情况下,子类方法被执行了。所以,子类中方法的参数范围(前置条件)必须与父类的参数范围(前置条件)相同或者更加宽松。

再来说一下第4个含义,覆写或实现父类的方法时,输出结果可以被缩小。

什么意思呢?父类方法的返回值是类型T,子类相同方法(重载或覆写)的返回值是S,那么里氏替换原则就要求S必须小于等于T。也就是说,要么S和T类型相同,要么S是T的子类。

对于覆写而言,父类和子类中的方法时一模一样的,所以返回类型也应当是一样的。

对于重载,也就是第3个含义所讲到的,子类的输入参数宽于或等于父类的参数,也就是说这个方法时不会被调用的。

相关文章

什么是设计模式一套被反复使用、多数人知晓的、经过分类编目...
单一职责原则定义(Single Responsibility Principle,SRP)...
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强...
适配器模式将一个类的接口转换成客户期望的另一个接口,使得...
策略模式定义了一系列算法族,并封装在类中,它们之间可以互...
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,...