前言
里氏替换原则是六大设计原则之一,基于java的多态和继承的特性,广泛使用与面向接口的编程。java泛型中的PECS原则,规则比较绕,但与里氏替换原则的使用惟妙惟肖,殊途同归。
一、里氏替换原则
接口是对业务抽象,一个接口可能对应不同的实现类,通过申明接口的方式可以做到解耦和隔离不同场景下具体的业务规则,将父类替换为它的各个子类对象,程序不会出现异常,里氏替换原则符合开闭原则,因此对子类的实现有了一下规则:
1)子类可以重写父类抽象方法,但是不能改变父类非抽象方法
2)子类可以增加自己 特有方法
3)子类的方法重载父类的方法时,方法的前置条件(方法的输入,入参)要比父类的入参更宽松
4)子类的方法实现父类的方法时(重写,重载,实现抽象方法),方法的后置条件(输出、返回值)要比父类更严格或相等。
如下代码,定义了Fruit接口,对应的实现类和子类:Watermelon和QilinWatermelon
public interface Fruit {
Fruit fork(FruitFork fork);
}
public class Watermelon implements Fruit{
// 重写方法的返回类型更加严格
@Override
public Watermelon fork(FruitFork fork) {
return null;
}
// 重载方法的参数宽松
public Watermelon fork(Fork fork) {
return null;
}
}
public class QilinWatermelon extends Watermelon {
@Override
public QilinWatermelon fork(FruitFork fork) {
return null;
}
public QilinWatermelon fork(Fork fork) {
return null;
}
}
在使用过程之中,如下代码模拟了面向接口的编程:
public static void main(String[] args) {
// 申明接口,子类对象赋值
Fruit fruitObj = new Watermelon();
// 实际返回值是Watermelon类型,Fruit wm 为类型已知的变量,fruitObj.fork返回值满足协变
Fruit wm = fruitObj.fork(new FruitFork());
// 当其它子类替换父类
fruitObj = new QilinWatermelon();
// 调用方法不受影响,无需修改参数类型代码
Fruit qinlin = fruitObj.fork(new FruitFork());
}
二、泛型PECS
定义泛型类和泛型方法可以提高通用性,如同一个模板,泛型类型可以在实例化对象时传入。
在申明泛型变量的时候可以使用泛型通配符<?>,代表未知类型。可以通过extends和super定义通配符的上下界限。
List<? extends Watermelon> fruits = new ArrayList<QilinWatermelon>();
List<? super Watermelon> fruitsSup = new ArrayList<Fruit>();
引入一个概念:父类大于子类。子类转化父类是协变,父类转化子类是逆变。
通配符广泛运用在集合容器类之中:extends确定了上限,无法确定类型E装了多小的子类,因此只能E本身或者父类用来取;super确定了下限,无法确定类型C装了多大的父类,因此只能用C本身或者子类来存。
PECS原则因此而诞生,全拼是"Producer Extends Consumer Super"。可以理解方法的入参是消费者(CS),方法的返回是生产者(PE)。
就如同里氏替换原则一样,List<? extends Fruit> fruits是对接口的申明,<?>是不确定哪一个实现子类,new ArrayList()则是实现类,赋值接口变量fruits。子类方法返回值需要更严谨,以满足协变,在PE里面,fruits (接口变量)返回值的类型必须是Watermelon或者其父类,如下代码:
List<? extends Watermelon> fruits = new ArrayList<QilinWatermelon>(); // PE
Fruit pe1 = fruits.get(0);
Watermelon pe2 = fruits.get(0);
在里氏替换原则里面,子类方法入参需要更宽松,在CS里面,fruitsSup(接口变量)方法的入参需要是Watermelon的子类,如下代码:
List<? super Watermelon> fruitsSup = new ArrayList<Fruit>(); // CS
fruitsSup.add(new Watermelon());
fruitsSup.add(new QilinWatermelon());