泛型之通配符

数组 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是所有类的父类,所以才可以这么用。

结论:当使用父界限定通配符时,泛型类中返回值为泛型的方法不能使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章