泛型之通配符

39次阅读

共计 4358 个字符,预计需要花费 11 分钟才能阅读完成。

数组 VS List 集合
数组类型为 Object,可以存储任意类型的对象,List 集合同样可以做到
Object[] obj = new Object[1];
List list = new ArrayList();
数组类型可以为 Integer 类型,专门存储 Integer 类型对象,List 集合同样也可以
String[] str = new String[1];
List<String> list = new ArrayList<>();
前两局双方打成平手,最后的决胜局。先看数组代码:
Object[] obj = new Integer[2];
obj[0] = 52021;
// 编译期 OK,运行期就挂了
obj[1] = “Full of confidence”;
上面的代码在运行期会抛出异常:java.lang.ArrayStoreException: java.lang.String
List 集合代码:
// 编译期报错
List<Object> obj = new ArrayList<String>();
最终结果,List 集合更胜一筹,因为它能尽早的发现错误。
分析最后一战中数组的代码:
在编译期间,编译器不会检查这样的赋值,编译器觉得它是合理的,因为父类引用指向子类对象,没有任何问题。赋值时,将字符串存储在一个 Object 类型的数组中也说的过去,但在运行时,发现明明内存分配的是存储整型类型对象的格子,里面却放着字符串类型的对象,自然会报错了。
分析最后一战中 List 集合的代码:
由于运用了泛型,在编译期就发现了错误,避免了将问题带到运行时。思考个问题,如果代码在编译期没有报错会发生什么?
在编译期没有报错并且在运行期会将泛型擦除,全部变为了 Object 类型。所以执行 obj.add(new Integer(1)) 也是可以的。如果真是这样的话,那么泛型还有什么存在的意义呢?所以这种假设是不存在的,所以会在编译期报错。
结论:在使用泛型时,泛型的引用和创建两端,给出的泛型变量必须相同。
通配符
通配符只能出现在等号左面,不能出现在 new 的一边。

List<?> list = new ArrayList<String>()
List<? extends Number> obj1 = new ArrayList<Integer>()
List<? super Integer> obj = new ArrayList<Number>()

无界通配符
public void foo() {
List<Integer> list = new ArrayList<Integer>();
//foo2(list); // 编译期报错
foo3(list); // 正常编译
}

public void foo2(List<Object> list) {//TODO}
public void foo3(List<Integer> list){//TODO}
上面代码中,调用 foo2 方法编译期报错原因:泛型的引用和创建两端,给出的泛型变量不一致,相当于:List<Object> list = new ArrayList<Integer>();
public void foo() {
List<Integer> list = new ArrayList<Integer>();
List<String> strList = new ArrayList<String>();
foo3(list); // 正常编译
}
public void foo3(List<Integer> list){//TODO}
现在希望将 strList 作为参数调用 foo3 方法,这时就想到了方法的重载,所以定义了一个重载的方法。public void foo3(List<String> list){//TODO} 定义完成后,竟然报错了,并且 foo3(List<Integer> list) 也报错了。这是由于泛型擦除导致的。在运行期,会有泛型擦除,所以 foo3(List<Integer> list) 与 foo3(List<String> list) 会变成一样的方法,所以在编译期就要报错,否则在运行期就无法区分了。
这里无法使用 foo3 方法重载,除非定义不同名字的方法。除了定义不同名字的方法之外,还可以使用通配符。
public void foo() {
List<Integer> list = new ArrayList<Integer>();
List<String> strList = new ArrayList<String>();
foo3(list);
foo3(strList);
}
public void foo3(List<?> list){//TODO}
调用 foo3(strList); 相当于:List<?> list = new ArrayList<String>(); => List<?> list = strList
通过使用通配符,foo3(List<?> list); 可以传递任何类型的对象,但也是有缺点的。使用通配符,赋值 / 传值的时候方便了,但是对泛型类中参数为泛型的方法起到了副作用。如何理解泛型类中参数为泛型的方法起到了副作用这句话呢?结合代码来看
/**
* 使用 ? 通配符
*/
public void foo3(List<?> list) {
//list.add(1); // 编译期报错,起到了副作用
//list.add(“hello”); // 编译期报错,起到了副作用
Object o = list.get(0); // 其实也是作废的,只是由于 Object 是所有类的父类,所以这里不会报错。
}
List 定义:接口 List<E>,E 代表是泛型类;List 中 add 方法定义:boolean add(E e),参数为 E,说明参数为泛型。List 接口使用通配符,调用 add 方法时,副作用是在编译期报错;泛型类中返回值为泛型的方法,也作废了,如:get 方法定义:E get(int index)
子界限定

子界限定:? extends Number
解释:? 是 Number 类型或者是 Number 的子类型
缺点:参数为泛型的方法不能使用

public void foo4() {
List<Integer> intList = new ArrayList<Integer>();
List<Long> longList = new ArrayList<Long>();
foo5(intList); //Integer 是 Number 的子类型
foo5(longList); //Long 是 Number 的子类型
}
public void foo5(List<? extends Number> list) {
//list.add(1); // 编译期报错
//list.add(2L); // 编译期报错
}
分析以上代码:
foo5(intList); 相当于 List<? extends Number> list = intList; 赋值操作,这是没有问题的;list.add(1); 则会在编译期报错。list 定义的类型是 List<? extends Number>,它带有泛型,而 add 方法的参数也是泛型类型,符合:泛型类中参数为泛型的方法起到了副作用这个结论。所以调用 add 编译期报错。
想想看,如果 list.add(1); 不报错:
foo4 中调用了 foo5(longList); 相当于 List<? extends Number> list = new ArrayList<Long>();, 然后执行 foo5,调用 list.add(1); 如果不报错也就相当于 Long 类型容器可以盛放 Integer 类型数据,这样一来,泛型也就没有意义了。有人也许会问,既然 add 方法会报错,为什么 foo5(longList); 没有问题?其实我觉得这是不一样的,调用 add 方法不确定因素很多,因为类型可能是 Integer, 也可能是 Long,人们无法保证在调用 add 方法时,只传递相同类型的变量,所以程序就直接限定死了,你不可以 add 任何东西。但直接赋值,这个值是可以确定的,类型具有统一性,要是什么都是什么,所以是可行的。
结论:当使用子界限定通配符时,泛型类中参数为泛型的方法不能使用。
也许有人会问,这样做是否可以确定类型?
List<? extends Number> list = new ArrayList<Integer>();
list.add(1); // 编译期报错
很遗憾,这样也是不行的。? 仍然代表了不确定性,所以编译器压根就是将这种方式的方法的参数类型是泛型的全部废掉了。
add 不能用,那赋值操作后可从中取值吗?答案是肯定的。Number number = list.get(0);
分析以上代码:
无论返回什么值,总归都是 Number 类型的,这是可以确定的,所以可以用 Number 接收返回值为泛型的方法。当然,这里说没有问题也只能是 Number 类型或者是 Object 类型接收,别的类型是不可以的。
结论:当使用子界限定通配符时,泛型类中返回值为泛型的方法可以使用。
父界限定

父界限定:? super Integer
解释:? 是 Integer 类型或者是 Integer 的父类型
缺点:返回值为泛型的方法不能使用

public void foo6() {
List<Number> numList = new ArrayList<Number>();
List<Integer> intList = new ArrayList<Integer>();
foo7(numList); //Number 是 Integer 的父类型
foo7(intList); //Integer 是本身
}
public void foo7(List<? super Integer> list) {
list.add(1);
}
分析以上代码:
list.add(1); 不会报错,因为类型可以确定。

如果 List<? super Integer> list 的赋值是 List<? super Integer> list = new ArrayList<Number>(); 没问题
如果 List<? super Integer> list 的赋值是 List<? super Integer> list = new ArrayList<Integer>(); 没问题
如果 List<? super Integer> list 的赋值是 List<? super Integer> list = new ArrayList<Object>(); 也没问题

所以只要 add(1) 就肯定没有问题,就是说 add(1),这个实参 1,符合任何一种情况。
结论:当使用父界限定通配符时,泛型类中参数为泛型的方法可以使用。
public void foo7(List<? super Integer> list) {
list.add(1);
Object obj = list.get(0);
}
Object obj = list.get(0); 这句话没有报错,但其实是作废的,只是由于 Object 是所有类的父类,所以才可以这么用。
结论:当使用父界限定通配符时,泛型类中返回值为泛型的方法不能使用。

正文完
 0