在网上发现这篇文章写得不错,地址:http://build.cthuwork.com:8081/wordpress/category/java教程/java再谈泛型/
首先本文假定读者对Java的泛型有基础的了解,若需要请参考其他资料配合阅读。
泛型的泛参(type argument)可以使用实际类型或者通配符(wildcard)。其中通配符可以通过边界(bound)来限制其接受的实际参数的类型。根据其种类,可以分为无界(unbounded)、上界(upper bound)和下界(lower bound)。其泛型边界决定了输入(input)和输出(output)分别能接受什么类型。
输入为其函数的参数、属性能够赋值的值的类型,输出为函数的返回值、获取到的属性的值的类型。
一、实际类型
泛型的泛参可以使用实际类型。也就是类似于List
Object[] array = new String[1];
array[0] = 12.450F;
这段代码是可以通过编译的,然而会让静态类型的Java语言在没有任何强制类型转换的情况下出现类型异常。我们尝试往一个String类型的数组索引为0的位置赋值一个Float类型的值,这当然是行不通和完全错误的。Java数组能够协变是一个设计上的根本错误,它能导致你的代码在你完全不知情的情况下崩溃和异常,但现在改已经为时已晚。幸好我们有和经常使用集合API,否则最常见的情况可能如下:
public Number evil;
public void setAll(Number[] array) {
for (int i = 0;i < array.length;i++) {
array[i] = evil;
}
}
public void looksGood() {
atSomewhereWeDontKnown(); //We summoned evil to our kawaii and poor code
Float[] ourKawaiiArray = getOurKawaiiArray(); //Oops
}
public void atSomewhereWeDontKnown() {
evil = 12450;
}
public Float[] getOurKawaiiArray() {
Float[] weWantFloatFilled = new Float[0xFF];
setAll(weWantFloatFilled); //Buts... we got (1)E(2)V(4)I(5)L(0)...
return weWantFloatFilled;
}
注:我试了一下,以上代码执行looksGood()时会出错,第4行报java.lang.ArrayStoreException。所以,不知道作者所说的we got (1)E(2)V(4)I(5)L(0)...是什么意思。可能是java某个老版本运行的结果。
我们可不想让(1)E(2)V(4)I(5)L(0)充满我们的代码。所以,泛型吸取了这个教训,本身就是为了提高类型安全性而设计的泛型不能犯这样的低级错误。所以你不能写以下代码:
List
这段代码在第一行就无法通过编译,因为你尝试协变一个泛型。其解决办法和其他的说明将在后续讨论。
二、通配符
1.无界通配符
无界通配符为”?”,可以接受任何的实际类型作为泛参。其能接受的输入和输出类型十分有限。
①可用输入类型
严格意义上不能接受任何的类型作为输入,考虑以下代码:
List> list = new ArrayList();
list.add("123");//报异常, list.add(null)正确
你可能觉得这段代码看起来没有问题。通常会这样考虑,我们可以简单的把无界通配符”?”看成Object,往一个Object类型的列表加一个String有什么问题?况且其实际就是String类型。其实并不能通过编译,这并不是编译器出现了错误。这里有个逻辑漏洞,我们仔细考虑无界通配符的意义。无界通配符代表其接受任何的实际类型,但这并不意味着任何的实际类型都可以作为其输入和输出。其语义上有微妙的但巨大的区别。其含义是不确定到底是哪个实际类型。可能是String,可能是UUID,可能是任何可能的类型。如果这是个UUID列表,那么往里面加String等就会出事。如果是String列表,往里面加UUID等也会出事。或者我们不管其是什么类型的列表,往里面加Object,然而Object里有你的实际类型的属性和方法么。即使实际是Object列表,我们也无法确定。那么,无界通配符就不能接受任何输入了么,看起来是这样。其实有个例外,null作为一个十分特殊的值,表示不引用任何对象。我们可以说String类型的值可以为null、UUID类型的值可以为null,甚至Object类型的值可以为null。无论是什么类型,都可以接受null作为其值,表示不引用任何对象。所以无界通配符的输入唯一可接受的是可为所有类型的null。
②可用输出类型
无界通配符的输出类型始终为Object,因为其意义为接受任何的实际类型作为泛参,而任何的实际类型都可以被协变为Object类型,所以其输出类型自然就为Object了。没有什么需要注意的地方。
2.上界通配符
上界通配符为”extends”,可以接受其指定类型或其子类作为泛参。其还有一种特殊的形式,可以指定其不仅要是指定类型的子类,而且还要实现某些接口。这种用法非常少用,我在很多开源项目中基本没看到这种用法。由于这和本章内容无关,不影响输入和输出的类型,所以暂不描述。
①可用输入类型
严格意义上同样不能接受任何的类型作为输入,出于严谨目的,我们再从头分析一遍,这次以Minecraft的源代码为例,考虑以下代码:
List extends EntityLiving> list = new ArrayList();
list.add(player);
你可能觉得这段代码又没问题了,EntityPlayer确实继承了EntityLiving。往一个EntityLiving的列表里加EntityPlayer有什么问题?放肆!12450!好不闹/w\。这里的问题在于如果实际上是EntityPig的列表呢。这么想你就应该懂了,和无界通配符差不多,其只是限定了列表必须是EntityLiving的子类而已,我们并不知道实际是什么。所以在这里我们只能添加EntityLiving类型的对象。是不是觉得有什么不对?对了,我就是超威蓝猫!好不闹/w\,我们能在EntityLiving上调用EntityPlayer的getGameProfile么,明显不能,况且我们到底能不能实例化EntityLiving也是个问题。这里真的很容易混淆概念,一定要牢记,只能使用null作为上界通配符的输入值。
②可用输出类型
好了,这次终于能玩了,上界通配符的输出类型为其指定的类型,实际上如果通配符位于泛型类的声明中例如:
public class Foo {
public T entity;
}
这个类中entity字段的实际类型不是所有类型的父类Object了,而是EntityLiving,这可以用查看字节码的方式证实。当然其类型是Object也不会有太大的差别,可以想到的问题是当我们以某种方式往其内部传入了Object类型或其他不是EntityLiving类型或其子类的对象时,可能会出现类型转换异常或者更严重的留下随时代码会崩溃的隐患。而直接使用EntityLiving类型作为其实际类型就会在尝试这么做的同时抛出类型转换异常,从而避免这种问题。
3.下界通配符
下界通配符为”super”,可以接受其指定类型或其父类作为泛参。可能很多人都没有用过下界通配符,因为其真的很少用。其主要用处之一是在使用Java或第三方的API的泛型类时,对泛参类型不同,但泛参具有继承关系,且主要关注其输入的泛型对象进行归纳。以Minecraft的源码为例,考虑以下代码:
private EntityMob ourKawaiiMob;
private EntityMob otherKawaiiMob;
public int compareMobEntity(Comparator super EntityMob> comparator) {
return comparator.compare(ourKawaiiMob, otherKawaiiMob);
}
此方法可以接受一个比较器,用于比较两EntityMob。这里的含义是,我们希望接受一个EntityMob或其父类的比较器。例如Comparator
①可用输入类型
下界通配符的输入类型为其指定的类型或子类。因为其意义为接受其指定类型或其父类作为泛参。那么无论我们提供的对象是什么类型,只要是其指定的类型或子类的对象,那么毫无例外一定是其指定的类型的对象。我们不能提供其指定的类型的父类作为对象,考虑以下代码:
private EntityLiving our;
private EntityLiving other;
Comparator super EntityMob> comparator = new EntityMobComparator();
comparator.compare(our, other);
这段代码不能通过编译,我们尝试用一个EntityMob的比较器来比较EntityLiving。不仔细考虑可能以为这并没有什么问题,EntityMob的比较器完全有能力来比较EntityLiving啊?但是实际情况是如果这段代码成功编译,而且没有动态类型检查的话EntityMob的比较器就可能会尝试其获取EntityLiving并没有的,属于EntityMob的属性,然后就会获取到非法的数据,或导致Java运行时崩溃,这当然是不行的。好在我们即使这么做了,Java也会强制抛出ClassCastException。
②可用输出类型
下界通配符的输出类型始终为Object,因为其意义为接受其指定类型或其父类作为泛参,我们并不知道具体是哪一个父类。而任何的实际类型都可以被协变为Object类型,所以其输出类型自然就为Object了。
三、回顾泛型边界和输入输出类型的区别
泛型边界并不直接代表着能接受的输入输出的类型,其含义为能接受什么样的实际类型。而输入输出类型能是什么则是根据泛型边界的含义得出的,其中的限制是由于我们只能通过泛型边界对实际类型进行猜测而产生的,希望大家能仔细理解其中的含义。
泛型系统是作为Java 5的一套增强类型安全及减少显式类型转换的系统出现的。泛型也叫参数化类型,顾名思义,通过给类型赋予一定的泛型参数,来达到提高代码复用度和减少复杂性的目的。
在Java中,泛型是作为语法糖出现的。在虚拟机层面,并不存在泛型这种类型,也不会对泛型进行膨胀,生成出类似于List
那么在Java中泛型是如何如何实现其目的的呢?Java的泛型充分利用了多态性。将无界(unbounded)的通配符(wildcard)理解为Object类型,因为Object类型是所有除标量(Scalar)以外,包括普通的数组和标量数组的类型的父类。将所有有上界(upper bound)的通配符理解为其上界类型例如
编译前
publicclass Foo {
privateT value;
publicvoid set(T value) {
this.value = value;
}
publicT get() {
returnthis.value;
}
publicstatic void main(String[] args) {
Foo foo = newFoo();
foo.set("foo");
String value = foo.get();
}
}
编译后:
publicclass Foo {
privateCharSequence value;
publicvoid set(CharSequence value) {
this.value = value;
}
publicCharSequence get() {
returnthis.value;
}
publicstatic void main(String[] args) {
Foo foo = newFoo();
foo.set("foo");
String value = (String) foo.get();
}
}