定义:在面向对象的程序设计中,里氏替换原则(Liskov Substitution principle)是对子类型的特别定义。里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程序中代替其基类(超类)对象。”
原理:在软件开发过程中,若子类重写了父类方法,当用子类代替父类时就会出现逻辑不一致的问题。
问题由来:若类A
实现了方法a
,而其子类A1
覆写了该方法,则当子类出现在父类定义的类型时可能会出错(针对继承时),如下:
public class Main {
public static void main(String[] args) {
A[] as = new A[]{
new A(), new A1()
};
for (A a : as) {
a.a();
}
}
}
class A {
public void a() {
System.out.println("A#a");
}
}
class A1 extends A {
@Override
public void a() {
System.out.println("A1#a");
}
}
output
A#a
A1#a
可以看到,数组中的第二个对象是A1
的实例,而A1
覆写来了方法a
,此时虽然定义的类型是A
,到那时由于A1
是A的子类,其是可以替换类型A
的对象的,而由于方法a
被覆写,输出内容超出了预想内容。
产生原因:软件开发时类型把控不严格可能会导致此问题
解决办法:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的形参要比父类方法的输入参数更宽松
- 当子类的方法实现父类的抽象方法时,方法的返回值应比父类更严格
应用场景:
针对继承时
如果继承是为了实现代码重用时,那么共享的父类方法应该保持不变,不能被子类重新定义,为了避免出现此类问题,我们应该在共享的父类方法中加上final
字段,防止子类不小心覆写了共享的父类方法。子类只能通过新添加方法来扩展功能,那么上面的代码应该修改为如下形式:
public class Main {
public static void main(String[] args) {
A[] as = new A[]{
new A(), new A1()
};
for (A a : as) {
a.a();
}
}
}
class A {
public final void a() {
System.out.println("A#a");
}
}
class A1 extends A {
public void a1() {
System.out.println("A1#a1");
}
}
A#a
A#a
可以看到,此时即便在A
类的数组中实例化的时A1
,我们调用方法a
时的逻辑还是A
的逻辑,而A1
需要扩展功能我们则新添加一个方法a1
,让其实现A1
需要扩展的功能。
针对多态时
如果继承的目的时为了多态,而多态的前提是子类覆盖并重新定义父类方法,为了符合里氏替换原则
,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类是,父类就不能实例化,所以也不存在可实例化的父类对象在程序里。也就规避了子类替换父类实例时逻辑不一致的问题。如下:
public class Main {
public static void main(String[] args) {
A[] as = new A[]{new A1(), new A2()};
for (A a : as) {
a.a();
}
}
}
abstract class A {
abstract void a();
}
class A1 extends A {
@Override
void a() {
System.out.println("A1#a");
}
}
class A2 extends A {
@Override
void a() {
System.out.println("A2#a");
}
}
A1#a
A2#a
此时A
类的定义是为了多态,同时我们将A
类设为了抽象类并将方法a
设为抽象方法,因此A
类不可以被实例化且所有子类都必须实现该方法,所以也就不存在父类逻辑,因此也就规避了子类与父类逻辑不一致的可能。注:若方法较多,且让子类选择实现时可以不设为抽象方法,单父类必须设为抽象类,且需要子类实现的方法必须为空方法。此时需要共享的方法要添加上final
关键字,防止子类不小心实现了共享方法。若我们希望某类不能被继承时也需要将类设为final
类。
小提示:final
关键字很重要,在必要时不要忘了加上它。添加final
关键字时不仅可以提高代码可读性同时也可以增加代码的性能。
网友评论