LSP与PECS原则
- LSP
LSP即“Liskov替换原则”,是面向对象最重要的几大原则(SOLID)之一;
个人的理解:LSP用于描述复用中的继承与实现的规则,即在子类型或实现类中:
- 子类型可以增加方法,但不可删
- 子类型需要实现抽象 类型 (接口、抽象类)中所有未实现的方法
- 子类型中重写的方法 必须有相同或子类型的返回值或者符合co-variant的参数
- 子类型中重写的 方法必须使用同样类型的参数或者符合contra-variant的参数(此种情况Java目 前按照重载overload处理)
- 子类型中重写的方 法不能抛出额外的异常
十分类似方法的可替代原则:
- 更强的不变量
- 更弱的前置条件
- 更强的后置条件
- PECS原则
重难点在于泛型中的LSP
由于泛型是类型不变的:
ArrayList<String> is a subtype of List<String>
List<String> is not a subtype of List <Object>
——即便两个相同的容器类中各自参数有着父类与子类的关系,可他们两个扯不上关系
原因在于类型擦除:类型参数在编译后被丢弃, 在运行时不可用,这使得虚拟机中没有泛型类型对象——所有对象都属于普通类。
那么当我们想让这两个容器类扯上关系(协变)时,就需要本文主角——Wildcards:通配符
- 无限定通配符——?
用法示例 :List<?>
我们写了一个打印list中所有元素的方法:
public static void printList(List<Object> list)
{
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
本意是期望可打印任意类型的List,但由于泛型不协变,只能打印 List<Object>
List<Ingeter> li = Arrays.asList(1, 2, 3);
printList(li);
实际运行时候却报错:
原因在于List<Ingeter>并非List<Object>的子类!无法利用类的多态性进行参数赋值!
此时代码修改为
public static void printList(List<?> list)
{
for (Object elem: list) System.out.print(elem + " ");
System.out.println();
}
成功运行,原因在于采用通配符实现两个泛型类的协变。
- Upper Bound(上限) 的通配符<? extends E>
<? extends E> 是 Upper Bound(上限) 的通配符,用来限制元素的类型的上限,比如
List<? extends Fruit> fruits;
表示集合中的元素类型上限为Fruit类型,即只能是Fruit或者Fruit的子类,因此对于下面的赋值是合理的
fruits = new ArrayList<Fruit>();
fruits = new ArrayList<Apple>();
如果集合中元素类型为Fruit的父类则会编译出错,比如
fruits = new ArrayList<Object>();//编译不通过
有了上面的基础,接着来看看 <? extends E>限定的集合的读写操作
1、写入
因为集合fruits中装的元素类型为Fruit或Fruit子类,直觉告诉我们,往fruits中添加一个Fruit类型对象或其子类对象是可行的
fruits.add(new Fruit());//编译不通过
fruits.add(new Apple());//编译不通过
结果是编译都不通过,为什么?
因为<? extends Fruit>只是告诉编译器集合中元素的类型上限,但它具体是什么类型编译器是不知道的,fruits可以指向ArrayList<Fruit>,也可以指向ArrayList<Apple>、ArrayList<Banana>,也就是说它的类型是不确定的,既然是不确定的,为了类型安全,编译器只能阻止添加元素了。举个例子,当你添加一个Apple时,但fruits此时指向ArrayList<Banana>,显然类型就不兼容了。当然null除外,因为它可以表示任何类型。
2、读取
无论fruits指向什么,编译器都可以确定获取的元素是Fruit类型,所有读取集合中的元素是允许的:
Fruit fruit = fruits.get(0);//编译通过
若秉持子类可以赋值父类,而父类不能赋值子类的多态性来理解上两条法则,将会很形象:

补充:<?>是<? extends Object>的简写
- Lower Bound(下限) 的通配符 <? super E>
用来限制元素的类型下限,比如
List<? super Apple> apples;
表示集合中元素类型下限为Apple类型,即只能是Apple或Apple的父类,因此对于下面的赋值是合理的
apples = new ArrayList<Apple>();
apples = new ArrayList<Fruit>();
apples = new ArrayList<Object>();
如果元素类型为Apple的子类,则编译不同过
apples = new ArrayList<RedApple>();//编译不通过
同样看看<? super E>限定的集合的读写操作
1、写入
因为apples中装的元素是Apple或Apple的某个父类,我们无法确定是哪个具体类型,但是可以确定的是Apple和Apple的子类是和这个“不确定的类”兼容的,因为它肯定是这个“不确定类型”的子类,也就是说我们可以往集合中添加Apple或者Apple子类的对象,所以对于下面的添加是允许的
apples.add(new Apple());
apples.add(new RedApple());
它无法添加Fruit的任何父类对象,举个例子,当你往apples中添加一个Fruit类型对象时,但此时apples指向ArrayList<Apple>,显然类型就不兼容了,Fruit不是Apple的子类
apples.add(new Fruit());//编译不通过
2、读取
编译器允许从apples中获取元素的,但是无法确定的获取的元素具体是什么类型,只能确定一定是Object类型的子类,因此我们想获得存储进去的对应类型的元素就只能进行强制类型转换了
Apple apple = (Apple)apples.get(0);//获取的元素为Object类型
同样依照上文多态的思想来理解:

问题来了,JDK1.5引入泛型的目的是为了避免强制类型转换的繁琐操作,那么使用泛型<? super E>干嘛呢?这里就得谈到泛型PECS法则了
- PECS法则
PECS法则:生产者(Producer)使用extends,消费者(Consumer)使用super
1、生产者
如果你需要一个提供E类型元素的集合,使用泛型通配符<? extends E>。它好比一个生产者,可以提供数据。
2、消费者
如果你需要一个只能装入E类型元素的集合,使用泛型通配符<? super E>。它好比一个消费者,可以消费你提供的数据。
3、既是生产者也是消费者
既要存储又要读取,那就别使用泛型通配符。

浙公网安备 33010602011771号