java零基础入门-高级特性篇(七) 泛型 下
本章阅读有难度,请谨慎阅读,如有不适,可以跳过。
本章继续讲解泛型的上下限和其他的知识点,由于概念的复杂性,这里继续使用Book这个类来描述,使概念理解起来具备连续性。
泛型的通配符可以分为3种类型,无边界通配符,设定上限的通配符,设定下限的通配符。
上一章讲解的<?>是无边界通配符,设定上限的通配符<? extends E>,设定下限的通配符<? super E>。
设定上限的通配符
首先来看一张图。有三个类,数学书继承教科书,教科书继承书籍。现在定义一个List<? extend Book> books集合,这是什么意思呢?<? extend Book>这个泛型表示通配符?匹配的类型只能是Book类型的子类,Book类型是?类型的上限,上限就是说这里?匹配的最高类型只能是Book了。
看图,如果设置通配符上限<? extend Book>,那么?可以是TextBook,也可以是MathBook,他们都是Book的子类。如果设置<? extend TextBook>,这时候通配符?的上限就是TextBook了,如果将Book类型作为通配类型,就会编译报错,而TextBook的子类MathBook作为通配类型是可以的。如果设置<? extend MathBook>,那么?只能是MathBook类型。
上例中主要看Student这个要读书的可怜孩子,readBook方法中设置了通配符的上限为Book,然后在主方法中设置的List泛型为MathBook,因为MathBook是Book的子类,所以满足通配符的条件,可以作为参数传递给readBook方法。这里要注意的是,设置通配符上限的时候依然不可以使用add方法。为什么?这里有点绕,绕不过来就假设。
假设通配符?是MathBook,那么参数就是List<MathBook>,但是往List<MathBook>里面添加Book类型,这是不行的,因为Book是MathBook的父类,所以设置通配符上限也不可以使用add方法。
再来看循环,在无边界通配符的时候,要变量元素只能是Object类型,但是这里可以作为Book类型遍历元素,为什么?因为通配符?已经设置了上限Book,无论?是什么类型,都是Book的子类,而子类是可以向上自动转型的,如果参数是List<MathBook>,依然可以使用Book类型来遍历MathBook元素。
设定下限的通配符
再来看设定下限的通配符。定义List<? super Book> books;使用<? super Book>的形式设置通配符的下限,意思是通配符?的类型只能是Book的父类,看图
设置<? super Book>以后,如果通配符?类型为TextBook,会编译错误,因为TextBook不是Book的父类,而是子类,MathBook是孙子,错的更离谱了。如果设置<? super TextBook>,那么?是Book类型就是允许的,因为Book是TextBook的父类,其他的请自行揣摩。
这里只需要修改Student类型,其他代码可以保持不变。需要注意的是这里设置了下限是MathBook,而传入的参数恰好是List<MathBook>,所以这里是可以的。如果将下限设置为TextBook,代码就会报错了,因为这里的参数只能是TextBook或者父类Book,传入List<MathBook>就会发生编译错误。
这里居然可以使用add方法了,为什么?假设通配符?是Book,那么List<Book>是可以添加Book的子类MathBook类型的。因为这里通配符不论是什么类型,必须是MathBook的父类,所以在父类的List集合添加子类MathBook是完全可以的。
至于循环中又变成了Object,是因为这里无法确定父类是什么类型,无法保证父类都有getName()这个方法。因为Object是Book的父类,如果参数是List<Object>,那么就无法使用Book里的方法了,所以只能当成Object来操作。
泛型方法
泛型方法?前面不是讲了么?请注意,泛型方法需要在定义方法的时候,就对方法中的泛型类型进行定义。
以上两个方法不是泛型方法,原因就是真正的泛型方法需要在方法中定义。如何定义泛型方法?
修饰符 <泛型类型参数> 返回值 方法名(){...}
请注意,在方法的修饰符与返回值之间定义泛型类型参数,这时候的方法才是一个泛型方法。泛型方法为什么要在定义方法的时候定义泛型?因为泛型是一个参数,参数就有作用域,定义在类上面的泛型作用域是整个类,定义在方法上的泛型,作用域是整个方法。
先看左边一张图,如果在类上面指定了泛型,而又在类中定义了泛型方法,而且泛型方法中的泛型参数和类中的泛型参数一样,那么类上的泛型类型参数会被方法中的泛型参数覆盖,程序也会出现警告。
原因就在右图,泛型类,是在实例化类的时候指明泛型的具体类型,泛型方法,是在调用方法的时候指明泛型的具体类型。就算泛型方法定义的泛型类型参数与类定义的不同也是可以的,因为方法自己定义了泛型参数,不需要类定义的泛型参数。在创建类对象的时候,具体定义的泛型类型可以和对象调用方法时,具体定义的泛型类型不同。比如Book在创建对象的时候使用的类型是Integer,而调用sayTheBookName的时候传递的参数却是String,这是完全可以的。如果定义了泛型方法,那么方法中的泛型可以看做是独立于类定义的泛型而存在的。所以如果定义泛型方法,建议方法中的泛型不要与类上定义的泛型类型相同。
然后,就算不使用泛型类,也是可以直接使用泛型方法的。比如上例中,去掉Book<T>后面的泛型定义,将T改为String,程序也不会报错,而且泛型方法可以正常被调用。
在使用泛型方法的时候有几个地方需要注意:
1)自动类型推断。比如book.sayTheBookName("教科书"),这里程序会根据传入的参数自动的将E推断为String类型。
2)在定义方法的时候,不要因为类型可以自动推断而定义相同的泛型类型参数。
这样定义泛型方法是没有问题的,可以正确编译,也可以正确运行。但是不建议这样做,因为根据传入的参数,第一个E会被推断为String类型,而第二个E被推断为Integer类型,这样会造成理解上的歧义。
3)如果直接将泛型类型参数定义为类型是不会报错的,但是如果在集合类型的泛型中,将泛型类型定义为一样的参数,就真的会报错了。
上面“教科书”和1很容易推断出是字符串和Integer类型,但是如果调用方法时将有泛型的集合作为参数,并且方法里面定义的集合泛型参数还是相同的,这时候程序就无法进行自动推断了。这里最好将泛型方法再多定义一个泛型参数,保证不会出现歧义,这样程序才能正确的进行类型推断。
public <M,O> int getAllNum(List<M> mathBook,List<O> englishBook){...}
这样就可以避免歧义,正确推断类型了。
泛型通配符和泛型方法
希望讲到这里你还没有晕。
那么我们继续看下一个问题。前面说的泛型通配符?可以代替任何一个类型,T这种形式的泛型类型参数不是也可以代替任何一个类型吗?他们有什么区别呢?
其实泛型方法和方法中使用通配符在某些情况下是可以相互替代的。
1)这是他们第一个相同的地方,他们都可以接收一个未知的类型
2)你可能会说,通配符可以设置上下限啊,不好意思,这个功能泛型方法也有
将上面的方法修改成通配符上限和泛型方法上限也没有任何问题。需要注意的是,使用泛型方法的上下限时,需要在方法定义的时候设置上下限,而不是在参数里面设置上下限。
不同的地方在于,当设置泛型通配符上下限的时候,会存在一个只能读不能写的情况,就是无法往集合添加元素,因为不能确定类型。但是使用泛型方法的时候,就可以对集合进行添加操作,因为调用泛型方法的时候,类型就已经确定了。所以如果需要对集合元素进行读取之外的操作,可以使用泛型方法。
再一个就是当多个泛型类型参数之间有依赖关系的时候,可以使用泛型方法。
这里有2个对象,依赖对象和被依赖对象,T extends B,T是依赖对象,B是被依赖对象。如果依赖对象不确定,可以使用泛型通配符,但是如果被依赖对象不确定,则不可以使用泛型通配符。
依赖对象使用通配符没有问题,程序可以运行。因为通配符类型的上限就是B。
如果被依赖对象不确定,则无法确定T类型的上限,导致程序编译出错。所以如果多个泛型类型之间有依赖关系,使用泛型方法会比较适合。
泛型的擦除
泛型类型信息只在编译的时候发挥作用,一旦被加载到虚拟机泛型信息会被全部丢弃。所以在编译阶段List<Book>和List<TextBook>可以看做两个不同的类型,但是一旦加载到虚拟机,他们就是同样的类型。泛型被丢了,那他是个什么类型?用专业的话说就是擦除到泛型的上限。比如没有指定上限的时候,擦除后的类型是Object,如果制定了类型的上限比如<? extends Book>,那么擦除后的类型就是Book。关于泛型的擦除会涉及到反射知识,这里老规矩,先混脸熟。
泛型知识一般多用于对代码进行高层次抽象,比如编写一些工具方法,框架,比如在集合框架中就有大量的泛型使用,所以有一定的难度,初学者掌握集合的泛型使用即可。