首页 » 编写高质量代码:改善Java程序的151个建议 » 编写高质量代码:改善Java程序的151个建议全文在线阅读

《编写高质量代码:改善Java程序的151个建议》建议97:警惕泛型是不能协变和逆变的

关灯直达底部

什么叫协变(covariance)和逆变(contravariance)?Wiki上是这样定义的:

Within the type system of a programming language, covariance and contravariance refers to the ordering of types from narrower to wider and their interchangeability or equivalence in certain situations(such as parameters, generics, and return types).

在编程语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性,简单地说,协变是用一个窄类型替换宽类型,而逆变则是用宽类型覆盖窄类型。其实,在Java中协变和逆变我们已经用了很久了,只是我们没发觉而已,看如下代码:


class Base{

public Number doStuff(){

return 0;

}

}

class Sub extends Base{

@Override

public Integer doStuff(){

return 0;

}

}


子类的doStuff方法返回值的类型比父类方法要窄,此时doStuff方法就是一个协变方法,同时根据Java的覆写定义来看,这又属于覆写。那逆变是怎么回事呢?代码如下:


class Base{

public void doStuff(Integer i){

}

}

class Sub extends Base{

public void doStuff(Number n){

}

}


子类的doStuff方法的参数类型比父类要宽,此时就是一个逆变方法,子类扩大了父类方法的输入参数,但根据覆写定义来看,doStuff不属于覆写,只是重载而已。由于此时的doStuff方法已经与父类没有任何关系了,只是子类独立扩展出的一个行为,所以是否声明为doStuff方法名意义不大,逆变已经不具有特别的意义了,我们来重点关注一下协变,先看如下代码是否是协变:


public static void main(Stringargs){

Base base=new Sub();

}


base变量是否发生了协变?是的,发生了协变,base变量是Base类型,它是父类,而其赋值却是子类实例,也就是用窄类型覆盖了宽类型。这也叫多态(Polymorphism),两者同含义,在Java世界里“重复发明”轮子的事情多了去了。

说了这么多,下面再来想想泛型是否也支持协变和逆变,答案是:泛型即不支持协变,也不支持逆变。很受伤是吧?为什么会不支持呢?

(1)泛型不支持协变

数组和泛型很相似,一个是中括号,一个是尖括号,那我们就以数组为参照对象,看如下代码:


public static void main(Stringargs){

//数组支持协变

Numbern=new Integer[10];

//编译不通过,泛型不支持协变

List<Number>ln=new ArrayList<Integer>();

}


ArrayList是List的子类型,Integer是Number的子类型,里氏替换原则(Liskov Substitution Principle)在此处行不通了,原因就是Java为了保证运行期的安全性,必须保证泛型参数类型是固定的,所以它不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。

泛型不支持协变,但可以使用通配符(Wildcard)模拟协变,代码如下所示:


//Number的子类型(包括Number类型)都可以是泛型参数类型

List<?extends Number>ln=new ArrayList<Integer>();


"?extends Number"表示的意思是,允许Number所有的子类(包括自身)作为泛型参数类型,但在运行期只能是一个具体类型,或者是Integer类型,或者是Double类型,或者是Number类型,也就是说通配符只是在编码期有效,运行期则必须是一个确定类型。

(2)泛型不支持逆变

Java虽然可以允许逆变存在,但在对类型赋值上是不允许逆变的,你不能把一个父类实例对象赋值给一个子类类型变量,泛型自然也不允许此种情况发生了,但是它可以使用super关键字来模拟实现,代码如下。


//Integer的父类型(包括Integer)都可以是泛型参数类型

List<?super Integer>li=new ArrayList<Number>();


"?super Integer"的意思是可以把所有Integer父类型(自身、父类或接口)作为泛型参数,这里看着就像是把一个Number类型的ArrayList赋值给了Integer类型的List,其外观类似于使用一个宽类型覆盖一个窄类型,它模拟了逆变的实现。

泛型既不支持协变也不支持逆变,带有泛型参数的子类型定义与我们经常使用的类类型也不相同,其基本的类型关系如表7-1所示。

注意 Java的泛型是不支持协变和逆变的,只是能够实现协变和逆变。