论里氏替换原则在java程序设计中的应用
1引言
在学习JAVA程序设计的过程中,常常需要继承来实现很多功能, OCP背后的主要机制是抽象和多态。在静态语言中,支持抽象和多态的关键机制是继承。正是使用了继承,才可以创建实现其基类中抽象方法的抽象类。继承机制确实带来了很多方便,子类拥有父类的所有方法和属性,从而可以减少创建类的工作量。提高了代码的重用性。提高了代码的扩展性,子类不但拥有了父类的所有功能,还可以添加自己的功能。但又有点也同样存在缺点:继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。降低了代码的灵活性。因为继承时,父类会对子类有一种约束。增强了耦合性。当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。有时修改了一点点代码都有可能需要对打断程序进行重构。如何扬长避短呢?方法是引入里氏替换原则肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑。其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的.
2 里氏替换原则的概念
定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2:所有引用基类的地方必须能透明地使用其子类的对象。
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。里氏代换原则是实现开放封闭原则的具体规范。这是因为: 实现开放封闭原则的关键是进行抽象,而继承关系又是抽象的一种具体实现,这样LSP就可以确保基类和子类关系的正确性,进而为实现开放封闭原则服务。如图:
里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法;子类中可以增加自己特有的方法;当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松;当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格;这四个原则的意思其实就是,子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法在我们做系统设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,这里使用的其实就是里氏替换原则。若子类不完全对父类的方法进行实例化,那么子类就不能被实例化,那么这个接口或抽象类就毫无存在的意义了.
3 应用实例
(1)里氏替换原则规定,子类不能覆写父类已实现的方法。
父类中已实现的方法其实是一种已定好的规范和契约,如果我们随意的修改了它,那么可能会带来意想不到的错误。下面举例说明一下子类覆写了父类方法带来的后果。
public class A {
public void fun(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
public class B extends A{
public void fun(int a,int b){
System.out.println(a+"-"+b+"="+(a-b));
}
}
public class demo {
public static void main(String[] args){
System.out.println(“父类的运行结果”);
A a=new A();
a.fun(1,2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.println(“子类替代父类后的运行结果”);
B b=new B();
b.fun(1,2);
}
}
运行结果:
父类的运行结果
1+2=3
子类替代父类后的运行结果
1-2=-1
我们想要的结果是“1+2=3”。可以看到,方法重写后结果就不是了我们想要的结果了,也就是这个程序中子类B不能替代父类A。这违反了里氏替换原则原则,从而给程序造成了错误。
有时候父类有多个子类,但在这些子类中有一个特例。要想满足里氏替换原则,又想满足这个子类的功能时,有的伙伴可能会修改父类的方法。但是,修改了父类的方法又会对其他的子类造成影响,产生更多的错误。这是怎么办呢?我们可以为这个特例创建一个新的父类,这个新的父类拥有原父类的部分功能,又有不同的功能。这样既满足了里氏替换原则,又满足了这个特例的需求。
(2)子类中可以增加自己特有的方法
这个很容易理解,子类继承了父类,拥有了父类和方法,同时还可以定义自己有,而父类没有的方法。这是在继承父类方法的基础上进行功能的扩展,符合里氏替换原则。
public class A {
public void fun(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
public class B extends A{
public void newFun(){
System.out.println(“这是子类的新方法…”);
}
}
public class demo {
public static void main(String[] args){
System.out.print(“父类的运行结果:”);
A a=new A();
a.fun(1,2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.print(“子类替代父类后的运行结果:”);
B b=new B();
b.fun(1,2);
//子类B的新方法
b.newFun();
}
}
运行结果:
父类的运行结果:1+2=3
子类替代父类后的运行结果:1+2=3
这是子类的新方法…
(3)当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
先看一段代码:
import java.util.HashMap;
public class A {
public void fun(HashMap map){
System.out.println(“父类被执行…”);
}
}
import java.util.Map;
public class B extends A{
public void fun(Map map){
System.out.println(“子类被执行…”);
}
}
import java.util.HashMap;
public class demo {
public static void main(String[] args){
System.out.print(“父类的运行结果:”);
A a=new A();
HashMap map=new HashMap();
a.fun(map);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.print(“子类替代父类后的运行结果:”);
B b=new B();
b.fun(map);
}
}
运行结果:
父类的运行结果:父类被执行…
子类替代父类后的运行结果:父类被执行…
我们应当主意,子类并非重写了父类的方法,而是重载了父类的方法。因为子类和父类的方法的输入参数是不同的。子类方法的参数Map比父类方法的参数HashMap的范围要大,所以当参数输入为HashMap类型时,只会执行父类的方法,不会执行fAD类的重载方法。这符合里氏替换原则。
但如果我将子类方法的参数范围缩小会怎样?看代码:
import java.util.Map;
public class A {
public void fun(Map map){
System.out.println(“父类被执行…”);
}
}
import java.util.HashMap;
public class B extends A{
public void fun(HashMap map){
System.out.println(“子类被执行…”);
}
}
import java.util.HashMap;
public class demo {
static void main(String[] args){
System.out.print(“父类的运行结果:”);
A a=newa3C/span> A();
HashMap map=new HashMap();
a.fun(map);
//父类存在的地方,都可以用子类替代
//子类B替代父类A
System.out.print(“子类替代父类后的运行结果:”);
B b=new B();
b.fun(map);
}
}
运行结果:
父类的运行结果:父类被执行…
子类替代父类后的运行结果:子类被执行…
在父类方法没有被重写的情况下,子方法被执行了,这样就引起了程序逻辑的混乱。所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。
(4)当子类的方法实现父类的(抽象)方法时,方法的后置条件(即方法的返回值)要比父类更严格
示例代码:
import java.util.Map;
public abstract class A {
public abstract Map fun();
}
import java.util.HashMap;
public class B extends A{
@Override
public HashMap fun(){
HashMap b=new HashMap();
b.put(“b”,“子类被执行…”);
return b;
}
}
import java.util.HashMap;
public%2%class demo {
%3span class=22hljs-keyword%!2 style=“color:rgb(133,153,0);”>public static void main(String[] args){
A a=new B();
System.out.println(a.fun());
}
}
运行结果:
{b=子类被执行…}
若在继承时,子类的方法返回值类型范围比父类的方法返回值类型范围大,在子类重写该方法时编译器会报错。
4 总结与展望
总之java采用的单继承相较于c++的多继承,总体上来看是“利”多于“弊”的。采用里氏替换原则可以让“利”的因素发挥最大的作用,并减少“弊”带来的诸多麻烦,在项目中,采用里氏替换原则时, 尽量避免子类的“ 个性”, 一旦子类有“ 个性”, 这个子类和父类 之间的关系就很难调和了, 把子类当做父类使用, 子类的“ 个性” 被抹杀—— 委屈 了点; 把子类单独 作为一个业务来使用, 则会让代码间的耦合关系变得扑朔迷离—— 缺乏类替换的标准。
里氏代换原则是很多其它设计模式的基础。它和开放封闭原则的联系尤其紧密。违背了里氏代换原则就一定不符合开放封闭原则。在软件项目设计中。本文从概念、作用、优势、应用思维入手,依托应用反思进行原则应用探讨。此原则在缩减软件设计 过 程 的 同 时,可显著提高软件项目的代码质量,在未来的研究中我们可以研究多态和里氏替换原则的关系,其他各个原则的结合在实践中的应用。