到底何时使用泛型方法?何时使用类型通配符呢?大多数时候都可以使用泛型方法来代替类型通配符。
这种场景下效果一样。
上面方法使用了泛型形式,这时定义泛型形参时设定上限(其中E是Collection接口里定义的泛型,在该接口里E可当成普通类型使用)。
1.什么时候用通配符?
上面两个方法中泛型形参T只使用了一次,泛型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
2.什么时候用泛型方法?
如果某个方法中一个形参(a)的类型或返回值的类型依赖于另一个形参(b)的类型,则形参(b)的类型声明不应该使用通配符—因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果形参(b)的类型无法确定,程序就无法定义形参(a)的类型。在这种情况下,只能考虑使用在方法签名中声明泛型,也就是泛型方法。
3.如果有需要,可以同时使用泛型方法和通配符,如Java的Collections.copy()方法。
上面copy方法中的dest和src存在明显的依赖关系,从源List中复制出来的元素,必须可以“丢进”目标List中,所以源List集合元素的类型只能是目标集合元素的类型的子类型或者它本身。但JDK定义src形参类型时使用的是类型通配符,而不是泛型方法。这是因为该方法无须向src集合中添加元素,也无须修改src集合里的元素,所以可以使用类型通配符,无须使用泛型方法。
简而言之,指定上限的类型通配符支持协变,因此这种协变的集合可以安全地取出元素(协变只出不进),因此无须使用泛型方法。当然,也可以将上面的方法签名改为使用泛型方法,不使用类型通配符,如下所示。
这个方法签名可以代替前面的方法签名。但注意上面的泛型形参 S,它仅使用了一次,其他参数的类型、方法返回值的类型都不依赖于它,那泛型形参S就没有存在的必要,即可以用通配符来代替S。使用通配符比使用泛型方法(在方法签名中显式声明泛型形参)更加清晰和准确,因此Java设计该方法时采用了通配符,而不是泛型方法。
类型通配符与泛型方法(在方法签名中显式声明泛型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的泛型形参必须在对应方法中显式声明。
补充几个知识点
1.泛型构造器—>菱形语法
正如泛型方法允许在方法签名中声明泛型形参一样,Java也允许在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器。一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型。
程序中①号代码不仅显式指定了泛型构造器中的泛型形参T的类型应该是String,而且程序传给该构造器的参数值也是String类型,因此程序完全正常。
但在②号代码处,程序显式指定了泛型构造器中的泛型形参T的类型应该是String,但实际传给该构造器的参数值是Double类型,因此这行代码将会出现错误。
Java 7新增的“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。
类的生命中用了泛型形参E,构造器用了泛型T,玩的溜!!!
2.泛型方法与方法重载
因为泛型既允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法定义。
上面的MyUtils类中包含两个copy()方法,这两个方法的参数列表存在一定的区别,但这种区别不是很明确:这两个方法的两个参数都是Collection对象,前一个集合里的集合元素类型是后一个集合里集合元素类型的父类。如果只是在该类中定义这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。例如,对于如下代码:
上面程序中粗体字部分调用copy()方法,但这个copy()方法既可以匹配①号copy()方法,此时泛型T表示的类型是Number;也可以匹配 ②号copy()方法,此时泛型T表示的类型是Integer。编译器无法确定这行代码想调用哪个copy()方法,所以这行代码将引起编译错误。
3.类型推断
Java 8改进了泛型方法的类型推断能力,类型推断主要有如下两方面。
1.可通过调用方法的上下文来推断泛型的目标类型。
2.可在方法调用链中,将推断得到的泛型传递到最后一个方法。
如下程序示范了Java 8对泛型方法的类型推断。
上面程序中前两行粗体字代码的作用完全相同,但第1行粗体字代码无须在调用MyUtil类的nil()方法时显式指定泛型参数为String,这是因为程序需要将该方法的返回值赋值给MyUtil类型,因此系统可以自动推断出此处的泛型参数为String类型。
上面程序中第3行与第4行粗体字代码的作用也完全相同,但第3行粗体字代码也无须在调用MyUtil类的nil()方法时显式指定泛型参数为Integer,这是因为程序将nil()方法的返回值作为了MyUtil类的cons()方法的第二个参数,而程序可以根据cons()方法的第一个参数(42)推断出此处的泛型参数为Integer类型。需要指出的是,虽然Java 8增强了泛型推断的能力,但泛型推断不是万能的,例如如下代码就是错误的。
因此,上面这行代码必须显式指定泛型的实际类型,即将代码改为如下形式:
4.擦除和转换
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限
类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时 ,所有在尖括号之间的类型信息都将被扔掉,比如一个List类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。
了一个List对象,这个List对象保留了集合元素的类型信息。当把这个List对象赋给一 个List类型的list后,编译器就会丢失前者的泛型信息,即丢失list集合里元素的类型信息,这是典型的擦除。
后续继续研究,感觉用处不是很大!
5.泛型与数组
Java泛型有一个很重要的设计原则—如果一段代码在编译时没有 提出“[unchecked] 未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,所以数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。但可以声明元素类型包含泛型变量或泛型形参的数组。也就是说,只能声明List[]形式的数组,但不能创建ArrayList[10]这样的数组对象。
假设Java支持创建ArrayList[10]这样的数组对象,则有如下程序:
在上面代码中,如果粗体字代码是合法的,经过中间系列的程序运行,势必在①处引发运行时异常,这就违背了Java泛型的设计原则。
如果将程序改为如下形式:
上面程序粗体字代码行声明了List[]类型的数组变量,这是允许的;但不允许创建List[]类型的对象,所以创建了一个类型为ArrayList[10]的数组对象,这也是允许的。只是把ArrayList[10]对象赋值给List[]变量时会有编译警告“[unchecked] 未经检查的转换”,即编译器并不保证这段代码是类型安全的。上面代码同样会在①处引发运行时异常,但因为编译器已经提出了警告,所以完全可能出现这种异常。
Java允许创建无上限的通配符泛型数组,例如new ArrayList<?>[10],因此也可以将第一段代码改为使用无上限的通配符泛型数组, 在这种情况下,程序不得不进行强制类型转换。正如前面所介绍的, 在进行强制类型转换之前应通过instanceof运算符来保证它的数据类
型。将上面代码改为如下形式(程序清单同上):
与此类似的是,创建元素类型是泛型类型的数组对象也将导致编译错误。如下代码所示。
由于类型变量在运行时并不存在,而编译器无法确定实际类型是什么,因此编译器在粗体字代码处报错。