Jdk1.5新特性之泛型(三)
在前面两章介绍了泛型概念、使用泛型、自定义泛型等等,在这一章,我们会更加深入的剖析泛型机制。
1.通配符
1.1使用类型通配符
假设有一个方法的参数接受一个集合,但是该集合里面存入的对象是不确定的,那我们又该怎么办呢?
我们会想到直接这样定义:public void test(List c){}
是的,这样定义没有错,但是编译器会给我们一个警告,还有好的办法吗?
我们可能会这样想,既然集合存入的对象类型不确定,那所有的对象都属于Object,那么我们就会想到这样定义:public void test(List<Object> c ){}, 但是我们仔细想想真的可以吗,这样的定义表面上看起来没有问题,这个方法声明也没有问题。但是我们调用该方法传入实际参数值时就会出现问题了。
如果这样调用: List<String> list = new ArrayList<String>();
Test(list);
那么问题就来,编译器会报错,为什么?在前面,我们说过,参数化类型不考虑类型参数的继承关系,也就是说这样子调用,相当于
List<Object> c = new ArrayList<String>();这样子编译就不过了。
为了解决这样的问题,java定义了一个类型通配符,即<?>
上面代码可以改成:public void test(List<?> c){}
这个 ? 就表示通配符,它的元素类型可以匹配任意类型。
但是用 ? 还是有局限的,看一下代码:
看以上代码,您可能觉得有点不能理解,程序为什么会报错呢?
当我们试图给集合添加一个字符串对象时,编译器报错了。
这是因为使用通配符之后,我们并不知道list集合到底装的是什么类型,它可能是String类型,也可能是Integer类型等等,所以不能向其中添加对象。查看jdk帮助文档,我们可以知道,add方法有类型参数E作为集合的元素类型,所以我们传给add的参数必须是E类型对象或其子类型对象,但是我们不知道E是什么类型,所以无法添加对象进去,不过,有一个例外,就是null类型,因为它是任何引用类型的子类型。
那么在集合中有多少方法不能用呢?查看API,只要方法接受的参数是E类型的就都不能使用。
1.2 设定通配符的上限
上面刚讲到,通配符可以匹配任意类型,也就是说List<?>可以是任何泛型List的父类(虽然参数化类型没有继承关系,但可以类比)。但是,现在我们不想匹配任何泛型List,只想匹配某类或某类的子类泛型List。
比如说,我们需要把动物类或者动物类的子类装进List集合,那该怎么做呢?可能有些人会这样子:因为Animal(动物类)是Cat(猫类)的父类,那就直接把Cat类交给Animal类就可以了。但是这样做真的可以吗?代码如下:
package wj.wcj.testwildcard;
//--------------------①
import java.util.ArrayList;
import java.util.List;
class Animal{
private String name;
public Animal(){}
public Animal(String name){
this.name=name;
}
@Override
public String toString() {
return "动物类。。。"+name;
}
}
class Cat extends Animal{
private String name;
public Cat(String name){
this.name=name;
}
@Override
public String toString() {
return "猫类。。。"+name;
}
}
class Dog extends Animal{
private String name;
public Dog(String name){
this.name=name;
}
@Override
public String toString() {
return "狗类。。。"+name;
}
}
public class WildCardTest {
public static void main(String[] args) {
List<Animal> animal List = new Array<>();
animalList.add(new Animal("小青"));
animalList.add(new Animal("强子"));
printAnimal(animalList);
List<Cat> CatlList = new ArrayList<>();
CatlList.add(new Cat("小白"));
CatlList.add(new Cat("小喵喵"));
printAnimal(CatlList); ☜
/*
* 报的错误:
* The method printAnimal(List<Animal>) in the type
* WildCardTest is not applicable for the arguments
* (List<Cat>)
*/
}
//--------------------②
public static void printAnimal(List<Animal> l) {
for(Animal animal : l){
System.out.println(l+",");
}
}
}
可以看到程序在☜这个位置报错了。这是我们忽略了一个细节,即数化类型没有继承关系(jdk1.5新特性(一)有讲到),你怎么可以把List<Cat> 对象赋给List<Animal> 对象呢。
为了解决这样的需求,java泛型提供了中被限制的通配符。可以表示为 List<? extends Animal> ,这表示该集合可以接收Animal类以及Animal的子类对象。
下面把第②段代码改为:
public static void printAnimal(List<? extends Animal> l) {
for(Animal animal : l){
System.out.println(l+",");
}
}
上面的错误就会消失了。List<? extends Animal>中的 ? 号代表一个未知类型,同样对于List<? extends Animal>集合,我们不能对它进行与类型(类型参数是E的)相关的操作,如add方法等。
1.3 设定通配符的下限
通配符的下限和通配符的上限类似,对上一个案例可以表示为
List<? super Cat>,它表示这个List可以接受Cat类以及Cat类的父类对象。代码案例可以对上面代码进行修改,这里就不给出了。
2.泛型方法与方法重载
泛型可以指定通配符的上限,也可以指定通配符的下限。那么有两个方法,一个用上限表示,另一个用下限表示,这两个是方法的重载吗?
public class TestUtils {
/* 报的错误:
* Method copy(Collection<T>, Collection<? extends T>) has the same
* erasure copy(Collection<E>, Collection<E>) as
* another method in type TestUtils
*/
public static <T> void copy(Collection<T> dest,Collection<? extends T> src){
}
/* 报的错误:
* Method copy(Collection<? super T>, Collection<T>) has the same
* erasure copy(Collection<E>, Collection<E>) as
* another method in type TestUtils
*/
public static <T> T copy(Collection<? super T> dest,Collection<T> src){
}
}
虽然我的英语不怎么好,但是我还是理解了这个是说这两个方法是相同的。也就是说,这两个方法并不是重载。那怎么会这样呢,跟我想象的完全不一样。这就牵扯到下面要说的,泛型的擦除了。因为在编译时,编译器会去掉类型化,那么两个方法的参数完全一样了,即都是Collection对象类型。
3.泛型与数组
Java的泛型有一个重要的设计原则,就是如果一段代码在编译时没有产生:“[unchecked]未经检查的转换”警告,那么程序在运行时就不会发生类型转换异常。正是因为这个,所以数组元素的类型不能包含类型变量或类型形参,不过无上限的类型通配符是可以的。虽然不能创建ArrayList<Integer>[10]这样的数组,但是给出ArrayList<String>[]的声明还是可以的。
如果java支持创建这样的泛型数组,那么有这样的程序:
//声明并创建一个List数组,而每个List对象放的是字符串对象
List<String>[] lsa =new ArrayList<String>[10];
//把List数组转换为Object数组
Object[] obj=(Object[])lsa;
//声明并创建一个List集合,存放Integer对象
List<Integer> intList = new ArrayList<Integer>();
//给集合添加一个Integer对象
intList.add(new Integer(5));
//将List集合对象作为Object数组的第一个元素存入数组中
obj[0]=intList;
//取出lsa数组的第一个元素List集合,并取出集合中的第一个元素
String s = lsa[0].get(0); //
如果第一句是成立的话,那么当程序执行到最后一句话时,会出现
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String,然而这么做违背泛型的设计原则。
Java允许创建无上限的通配符泛型数组,将上面代码改为:
//声明并创建一个List数组,而每个List对象放的是字符串对象
List<?>[] lsa = new ArrayList<?>[10];
//把List数组转换为Object数组
Object[] obj=(Object[])lsa;
//声明并创建一个List集合,存放Integer对象
List<Integer> intList = new ArrayList<Integer>();
//给集合添加一个Integer对象
intList.add(new Integer(5));
//将List集合对象作为Object数组的第一个元素存入数组中
obj[0]=intList;
//取出lsa数组的第一个元素List集合,并取出集合中的第一个元素
String s = (String)lsa[0].get(0);
可以看到在这时程序不得不进行强制类型转换(最后一句),最后一句运行时会引发类型转化异常。所以这是我们应该用instanceof运算符保证它的数据类型。
最后一句代码改为:
Object target=lsa[0].get(0);
if(target instanceof String){
String s = (String)target;
}
4.泛型与反射
4.1.Java在泛型出来时,反射包也被翻新了,来为参数化类型和方法提供参数信息。可以查看javaAPI:
| asSubclass(Class<U> clazz) | |||
static Class<?> | ||||
static Class<?> | forName(String name, boolean initialize, ClassLoader loader) | |||
| getAnnotation(Class<A> annotationClass) | |||
Class<?>[] | getClasses() | |||
Class<?> | getComponentType() | |||
getConstructor(Class<?>... parameterTypes) | ||||
Constructor<?>[] | getConstructors() | |||
getDeclaredConstructor(Class<?>... parameterTypes) | ||||
Constructor<?>[] | getDeclaredConstructors() | |||
Class<?> | getDeclaringClass() | |||
Class<?> | getEnclosingClass() | |||
Constructor<?> | getEnclosingConstructor() | |||
newInstance() |
4.2利用反射越过编译器对字节码操作
我们都知道如果一个集合声明并创建为这样:
List<String> listStr = new ArrayList<>();
那么我们就不能向List集合中添加Integer类型的对象了,但是真的一点办法都没有吗?当然不是,利用反射就可以做到,看以下代码:
List<Integer> listStr = new ArrayList<Integer>();
Class clazz = listStr.getClass();
Method m=clazz.getMethod("add",Object.class);
m.invoke(listStr, "java");
System.out.println(listStr.get(0));
这段代码最后打印的是字符串 java ,也就是说把字符串添加到了List<Integer> 集合里了。
这是怎么做到了呢?我们知道泛型的概念是在编译期存在的,在编译时编译器会将泛型擦除,所以在虚拟机中List<Integer>就变成了List原始类型了,当然原始类型的List可以存储任意对象。而反射就越过编译期,自然就不存在类型检查的问题了。
5.泛型的约束与局限性
(1)不能用基本类型实例化类型参数。也就是说没有List<int>,只有List<Integer> ,当然这原因就是类型擦除,擦除后List含有Object类型的域,而Object不能存int(当然自动封装是另一回事)。
(2)运行时类型查询只适用于原始类型。
(3)不能创建参数化类型数组。
(4)不能实例化类型变量。也就是说不能使用像new T(...),
new T[...]或T.class这样的表达式的类型变量。
(5)泛型类的静态上下文中类型变量无效,即不能在静态域或方法中引用类型变量。
(6)不能抛出或捕获泛型类的实例。