资讯专栏INFORMATION COLUMN

泛型之通配符

PingCAP / 2345人阅读

摘要:调用相当于通过使用通配符,可以传递任何类型的对象,但也是有缺点的。使用通配符,赋值传值的时候方便了,但是对泛型类中参数为泛型的方法起到了副作用。结论当使用父界限定通配符时,泛型类中返回值为泛型的方法不能使用。

数组 VS List

第一回合
数组类型为Object,可以存储任意类型的对象,List集合同样可以做到

Object[] obj = new Object[1];
List list = new ArrayList();

第二回合
数组类型可以为Integer类型,专门存储Integer类型对象,List集合同样也可以

Integer[] integer = new Integer[1];
List 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 obj = new ArrayList();

最终结果,List集合更胜一筹,因为它能尽早的发现错误。

分析最后一战中数组的代码:

在编译期间,编译器不会检查这样的赋值,编译器觉得它是合理的,因为父类引用指向子类对象,没有任何问题。赋值时,将字符串存储在一个Object类型的数组中也说的过去,但在运行时,发现明明内存分配的是存储整型类型对象的格子,里面却放着字符串类型的对象,自然会报错了。

分析最后一战中List集合的代码:

由于运用了泛型,在编译期就发现了错误,避免了将问题带到运行时。
思考个问题,如果代码在编译期没有报错会发生什么?

在编译期没有报错并且在运行期会将泛型擦除,全部变为了Object类型。所以执行obj.add(new Integer(1))也是可以的。如果真是这样的话,那么泛型还有什么存在的意义呢?所以这种假设是不存在的,所以会在编译期报错。编译期报错的原因就是在使用泛型时,泛型的引用和创建两端,给出的泛型变量不相同,所以在使用泛型时,泛型的引用和创建两端,给出的泛型变量必须相同。

通配符

通配符只能出现在等号左面,不能出现在new的一边。

List list = new ArrayList()

List obj1 = new ArrayList()

List obj = new ArrayList()

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

public void foo2(List list) {//TODO}
public void foo3(List list){//TODO}

上面代码中,调用foo2方法编译期报错原因:泛型的引用和创建两端,给出的泛型变量不一致,相当于:List list = new ArrayList();

public void foo() {
    List list = new ArrayList();
    List strList = new ArrayList();
    foo3(list); //正常编译
}
public void foo3(List list) {
    //TODO
}

现在希望将strList作为参数调用foo3方法,这时就想到了方法的重载,所以定义了一个重载的方法。public void foo3(List list){//TODO}
定义完成后,竟然报错了,并且foo3(List list)也报错了。这是由于泛型擦除导致的。在运行期,会有泛型擦除,所以foo3(List list)foo3(List list)会变成一样的方法,所以在编译期就要报错,否则在运行期就无法区分了。

这里无法使用foo3方法重载,除非定义不同名字的方法。除了定义不同名字的方法之外,还可以使用通配符。

public void foo() {
    List list = new ArrayList();
    List strList = new ArrayList();
    foo3(list);
    foo3(strList);
}
public void foo3(List list) {
    //TODO
}

调用foo3(strList);相当于:
List list = new ArrayList(); => List list = strList

通过使用通配符,foo3(List list);可以传递任何类型的对象,但也是有缺点的。使用通配符,赋值/传值的时候方便了,但是对泛型类中参数为泛型的方法起到了副作用。如何理解泛型类中参数为泛型的方法起到了副作用这句话呢?结合代码来看

/**
 * 使用 ? 通配符
 */
public void foo3(List list) {
    //list.add(1); //编译期报错,起到了副作用
    //list.add("hello"); //编译期报错,起到了副作用
    Object o = list.get(0); //其实也是作废的,只是由于Object是所有类的父类,所以这里不会报错。
}

List定义:接口ListE代表是泛型类;List中add方法定义:boolean add(E e),参数为E,说明参数为泛型。List接口使用通配符,调用add方法时,副作用是在编译期报错;泛型类中返回值为泛型的方法,也作废了,如:get方法定义:E get(int index)

子界限定

子界限定:? extends Number

解释:? 是 Number 类型或者是 Number 的子类型

缺点:参数为泛型的方法不能使用

public void foo4() {
    List intList = new ArrayList();
    List longList = new ArrayList();
    foo5(intList); //Integer是Number的子类型
    foo5(longList); //Long是Number的子类型
}
public void foo5(List list) {
    //list.add(1); //编译期报错
    //list.add(2L); //编译期报错
}

分析以上代码:

foo5(intList);相当于List list = intList;赋值操作,这是没有问题的;list.add(1);则会在编译期报错。
list定义的类型是List,它带有泛型,而add方法的参数也是泛型类型,符合:泛型类中参数为泛型的方法起到了副作用这个结论。所以调用add编译期报错。

想想看,如果list.add(1);不报错:

foo4中调用了foo5(longList);相当于List list = new ArrayList();,然后执行foo5,调用list.add(1);如果不报错也就相当于Long类型容器可以盛放Integer类型数据,这样一来,泛型也就没有意义了。有人也许会问,既然add方法会报错,为什么foo5(longList);没有问题?其实我觉得这是不一样的,调用add方法不确定因素很多,因为类型可能是Integer,也可能是Long,人们无法保证在调用add方法时,只传递相同类型的变量,所以程序就直接限定死了,你不可以add任何东西。但直接赋值,这个值是可以确定的,类型具有统一性,要是什么都是什么,所以是可行的。

结论:当使用子界限定通配符时,泛型类中参数为泛型的方法不能使用

也许有人会问,这样做是否可以确定类型?

List list = new ArrayList();
list.add(1); //编译期报错

很遗憾,这样也是不行的。? 仍然代表了不确定性,所以编译器压根就是将这种方式的方法的参数类型是泛型的全部废掉了。

add不能用,那赋值操作后可从中取值吗?答案是肯定的。
Number number = list.get(0);

分析以上代码:

无论返回什么值,总归都是Number类型的,这是可以确定的,所以可以用Number接收返回值为泛型的方法。当然,这里说没有问题也只能是Number类型或者是Object类型接收,别的类型是不可以的。

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

父界限定

父界限定:? super Integer

解释:? 是Integer类型或者是Integer的父类型

缺点:返回值为泛型的方法不能使用

public void foo6() {
    List numList = new ArrayList();
    List intList = new ArrayList();
    foo7(numList); //Number是Integer的父类型
    foo7(intList); //Integer是本身
}
public void foo7(List list) {
    list.add(1);
}

分析以上代码:

list.add(1);不会报错,因为类型可以确定。

如果List list的赋值是List list = new ArrayList();没问题

如果List list的赋值是List list = new ArrayList();没问题

如果List list的赋值是List list = new ArrayList();也没问题

所以只要add(1)就肯定没有问题,就是说add(1),这个实参1,符合任何一种情况。

结论:当使用父界限定通配符时,泛型类中参数为泛型的方法可以使用

public void foo7(List list) {
    list.add(1);
    Object obj = list.get(0);
}

Object obj = list.get(0);这句话没有报错,但其实是作废的,只是由于Object是所有类的父类,所以才可以这么用。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73385.html

相关文章

  • Java 型之上界下界配符

    摘要:泛型之上界下界通配符本教程是为编写的。这是在使用泛型编程时一个常见的误解,也是一个需要学习的重要概念。通配符使用指南学习使用泛型编程时,更令人困惑的一个方面是确定何时使用上限有界通配符以及何时使用下限有界通配符。 Java 泛型之上界下界通配符 本Java教程是为JDK 8编写的。本页描述的示例和实践没有利用后续版本中引入的改进。 泛型,继承和子类 如你所知,只要类型兼容,就可以将一种...

    shiyang6017 评论0 收藏0
  • 型之泛型

    摘要:定义具有一个或多个类型变量的类,称之为泛型类。泛型类的继承创建对象的两种方式错误方式错误原因继承了泛型类,但并不是泛型类,所以不能这样创建对象。同样是泛型类,它的父类也是泛型类,它传递的是常量。 泛型类 public class A { //在成员变量上使用泛型 private T t; public A() {} //构造参数类型上...

    caoym 评论0 收藏0
  • 型之泛型方法

    摘要:泛型方法显式赋值张三李四王五隐式赋值,常用此方式,可以不指定张三李四王五泛型方法不受类的限制,也就是说,即使方法所在的类不是泛型类,也可以定义泛型方法在泛型类中定义的方法,也不一定是泛型方法,就看你如何定义了。泛型类中可以定义泛型方法。 public class F { //泛型方法 public static T getT(T[] array...

    sydMobile 评论0 收藏0
  • 浅析Java泛型

    摘要:泛型类在类的申明时指定参数,即构成了泛型类。换句话说,泛型类可以看成普通类的工厂。的作用就是指明泛型的具体类型,而类型的变量,可以用来创建泛型类的对象。只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。 什么是泛型? 泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,...

    godiscoder 评论0 收藏0
  • JAVA泛型笔记

    摘要:泛型类泛型类和普通类的区别就是类定义时,在类名后加上泛型声明。泛型类的内部成员方法就可以使用声明的参数类型。 泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type),即所操作的数据类型在定义时被指定为一个参数。当我们使用的时候给这个参数指定不同的对象类型,就可以处理不同的对象。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和...

    n7then 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<