浏览以下内容前,请点击并阅读 声明
6 类型推测
java编译器能够检查所有的方法调用和对应的声明来决定类型的实参,即类型推测,类型的推测算法推测满足所有参数的最具体类型,如下例所示:
//泛型方法的声明
static <T> T pick(T a1, T a2) { return a2; }
//调用该方法,根据赋值对象的类型,推测泛型方法的类型参数为Serializable
//String和ArrayList<T>都实现接口Serializable,后者是最具体的类型
Serializable s = pick("d", new ArrayList<String>());
6.1 泛型方法的类型推测
类型的推测可以使泛型方法的使用语法和普通的方法一样,不必指定尖括号内的类型,如上述例子。
6.2 泛型类的类型推测
对于泛型类的使用,java编译器也可以进行类型的推测,因此调用泛型类时,可以不用指定尖括号内的类型参数,不过尖括号不可省略,之前的总结已经提到,空的尖括号又叫钻石(中文怪怪的),如下例所示:
//以下用法没有指定类型参数,尖括号为空
Map<String, List<String>> myMap = new HashMap<>();
//注意,空的简括号不能省略,如下代码编译器会发出警告
Map<String, List<String>> myMap = new HashMap();
上述代码中的第二个赋值语句中new HashMap() 实际是用的原始类型。
6.3 非泛型类的泛型构造器的类型参数推测
无论是泛型还是非泛型的类都可以使用泛型的构造器,如方法一样。
//类定义
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}
//以下是实例化以上类的表达式
new MyClass<Integer>("")
以上代码中的实例化表达式虽然没有指定构造器的类型参数,但是可以根据传入的参数推测其类型参数为String。
java7以前的版本能够推测出构造其的参数类型,而java7以后,使用钻石的语法也推测泛型类的参数类型。
需要注意的是,类型参数的推测算法只会使用传入的参数,目的类型或者和明显的返回类型来推测类型。
6.4 目的类型
java编译器充分利用了目的类型来推测泛型方法或者类的类型参数,如下例:
//Collections中的一个方法的声明如下
static <T> List<T> emptyList();
//现在调用该方法
List<String> listOne = Collections.emptyList();
以上中的第二个语句中,listOne变量类型为List<string>,就是目的类型,所以需要方法emptyList的返回类型也必须是List<Stirng>,这样可以推测泛型方法声明中的T为String,java7和8都可以实现这样的推测,当然你可以在调用泛型方法时指明方括号中的类型参数。
值得注意的是java7中方法的参数还不属于目的类型,而java8则把方法参数加入目的类型,如下例所示:
//如下方法接受的参数为List<String>
void processStringList(List<String> stringList) {
// process stringList
}
//Collections中的emptyList方法的签名如下
static <T> List<T> emptyList();
//java7中,下列调用语句的编译会报错,而java8则不存在这样的问题
processStringList(Collections.emptyList());
7 通配符
在泛型的代码中问号(?)代表通配符,代表未知的类型,通配符可以用在许多场合,可用作参数,字段,返回值的类型,但是通配符不能用作方法调用,泛型实例的创建和父类型的实参。
7.1 上限通配符
利用上限通配符可以放松对变量的限制。
上限通配符的声明方法如下例所示:
public static void process(List<? extends Foo> list) { /* ... */ }
上述声明的方法,的泛型参数使用了上限通配符,通配符"?"加extends关键词后跟其上限,此处的extends类似于通常意义上的extends和implements,意思是该方法是针对于Number类型的子类型,包括Integer,Float等的列表。
通配符<? extends Foo>匹配所有的Foo的子类型和Foo类型自身。
7.2 无限制通配符
无限制通配符就是简单的"?",如List<?>就代表未知类型的列表,以下两种情况适合使用无限制通配符:
- 声明一个要用到继承的Object类中的方法时
- 当代码中需要用到不依赖于类型参数的泛型类的方法时, 如List.size或者List.clear,而Class<?> 经常被用到,因为Class<T>中的许多方法是不依赖于类型参数T的。
以下例子很好的说明使用Object类中的方法时使用无限制通配符的好处:
//普通的方法声明
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
//使用通配符的泛型作为方法参数,该方法的参数能够传入任何类型的列表(List)
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
注意:既然定义了列表List<?>的类型的广泛性,就要承担广泛性的造成的后果,在方法声明中,只能对List<?>类型的变量插入null,因为你无法预知传入方法的类型变量,而List<Object>作为参数则可以插入任何类型的对象。
7.3 下限通配符
与上限通配符类似,下限通配符指定了类型参数的下限,未知的类型必须是指定类型的父类型,下限通配符的写法:<? super A>,此处关键词为super。
注意:不能同时指定上限和下限。
7.4 通配符和子类型
之前提到过,泛型之间的关系不仅仅是由他们的类型实参决定的,如不能说List<Number>就是List<Integer>的父类,不过使用通配符可以构成如下关系:
箭头表示“是其子类型”的关系,如List<Integer>是List<? extends Integer>的子类型,可以这样理解:List<Integer>是一种List<? extends Integer>。
7.5 通配符的捕获与辅助方法
有时候编译器会推测通配符的类型,如果一个字段的类型被定义为List<?>,当运算一个表达式的时候,编译器会从代码中推测该字段为一个特定的类型,这就叫通配符的捕获。
import java.util.List;
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0));
}
}
上述代码会编译出错,foo方法调用List.set(int,E),编译器首先将set方法内作为参数的i视为Object类型,无法判断将要插入的对象类型是否和目标列表类型是否一致,所以编译不能通过。
此时可以加入一个辅助方法,使其能能够顺利通过编译:
public class WildcardFixed { void foo(List<?> i) {
fooHelper(i);
}
// 创建辅助方法,调用该方法可以通过类型推测来实现通配符的捕获
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}
再来看一下一个例子:
import java.util.List; public class WildcardErrorBad { void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
Number temp = l1.get(0);
l1.set(0, l2.get(0));
l2.set(0, temp);
}
}
上述代码中的方法功能是将两个列表的首个元素交换,然而无法判断两个传入的实参的类型参数是否兼容,所以,无法编译通过,此处代码本质上就是错误的,没有相应的辅助方法。
7.6 通配符使用原则
泛型的使用有一点让人疑惑的就是不知道什么时候该用上限通配符,什么时候使用下限通配符,一下是几点原则:
为了说明问题,先列出两种变量1)In变量:作为代码中的数据来源,比如复制的方法copy(src,dest)中的src参数就是in变量,;2)out变量,在代码中用来存储数据作为他用,如copy(src,dest)中的dest参数就是out变量。变量列出之后,说原则:
- in变量使用上限通配符,使用extends关键词
- out变量使用下限通配符,使用super关键词
- 当需要使用的in变量可以通过Object类中的方法访问时,使用无限制通配符
- 当代码中既需要访问的变量既要当做in变量使用,又要当做out变量使用时,不要使用通配符
上述原则不试用与方法的返回类型,不建议在返回类型中使用通配符,否则将必须处理通配符的问题。