想了解泛型方法,首先就要知道为什么会出现这种泛型方法的原因,我们先来考虑下面的情景。我们现在要实现这样一个方法,该方法负责将一个Object数组添加到一个Collection集合中,OK,现在我们来写代码:
public class Test { //定义一个方法,将一个数组里面的元素复制到一个集合中 public static void fromArrayToCollection(Object[] array, Collection<Object> c) { for (Object object : array) { c.add(object); } } public static void main(String[] args) throws Exception { //下面的使用没有问题 Object[] array = {1,2,3}; Test.fromArrayToCollection(array, new ArrayList<Object>()); //下面的使用有问题,报错 String[] array1 = {"1","2","3"}; Test.fromArrayToCollection(array, new ArrayList<String>()); } }我们前面已经说过了,List<String>并不是List<Object>的子类,所以说上面的代码调用报错,也就是说我们前面定义的那个复制的方法真的是限制太大,如果我们想复制一种特定的类型的数据就要重新定义一个方法,这样子显然是不合理的,那么我们考虑下如果我们使用通配符List<?>是否可行呢?明显的也是不行的,因为java不允许把对象放进一个未知类型的集合中。
- 1,定义泛型方法
修饰符 <T,S> 返回值类型 方法名(形参列表) { 具体的方法体。。。 }
上面的泛型方法的语法值得注意的就是:所有的类型形参申明都应该放在方法修饰符和方法返回值类型之间。
现在我们使用泛型方法修改下前面我们的代码,实现可以将一个任意类型的数组赋值到一个相同类型的集合中。
//定义一个方法,将一个数组里面的元素复制到一个集合中
public static <T> void fromArrayToCollection(T[] array, Collection<T> c) { for (T object : array) { c.add(object); } } public static void main(String[] args) throws Exception { //下面的使用没有问题 Object[] array = {1,2,3}; Test.fromArrayToCollection(array, new ArrayList<Object>()); //下面的使用没有问题,集合可以省去类型 String[] array1 = {"1","2","3"}; Test.fromArrayToCollection(array1, new ArrayList<>()); //下面的使用没有问题,集合使用父类也同样没问题 Integer[] array2 = {1,2,3}; Test.fromArrayToCollection(array2, new ArrayList<Number>()); }
- 2,调用泛型方法
//定义一个方法,将一个数组里面的元素复制到一个集合中 public static <T> void fromArrayToCollection(T[] array, Collection<T> c) { for (T object : array) { c.add(object); } } public static void main(String[] args) throws Exception { //下面的使用没有问题 Object[] array = { 1, 2, 3 }; Test.fromArrayToCollection(array, new ArrayList<Object>()); //下面的使用没有问题,集合可以省去类型 String[] array1 = { "1", "2", "3" }; Test.<String> fromArrayToCollection(array1, new ArrayList<>()); //下面的使用没有问题,集合使用父类也同样没问题 Integer[] array2 = { 1, 2, 3 }; Test.<Number> fromArrayToCollection(array2, new ArrayList<Number>()); }
- 3,泛型方法和类型通配符的区别
boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c);上面的2个方法的形参都采用了类型通配符的形式,我们当然也可以采用泛型方法的形式:
<T> boolean containsAll(Collection<T> c) { return false; }; <T> boolean addAll(Collection<T> c){ return false; };对比一下上面的2种实现,我们发现我们在使用泛型方法这种形式时,上面的类型参数T只使用了一次,类型参数T这里只是产生了一个效果,就是说可以在不同的调用点传入不同的实际类型,显然这个时候应该用类型通配符更加合适,通配符就是被设计用来支持灵活的子类化的。
总结一下,什么时候使用泛型方法,什么时候使用通配符?
1),通配符用来支持灵活的子类化,泛型方法允许类型参数被用来表示方法的一个或者多个参数之间的类型依赖关系,或者方法返回值与参数之间的关系。比如没有这样子的依赖关系就应该使用泛型方法。
2),当然我们也可以同时使用泛型方法和通配符,比如下面代码:
public static <T> void copy(List<T> dest,List<? extends T> src){}
3),类型通配符既可以在方法签名中定义形参的类型,还可以用于定义变量的类型,但是泛型方法中的类型形参必须在对应方法中显式声明。
- 4,菱形语法与泛型构造器
public class Test { //泛型构造器 public <T> Test(T name) { System.out.println(name.toString()); } public static void main(String[] args) throws Exception { //泛型构造器的T参数类型是Integer new Test(1); //显式指定构造器的T参数类型是Integer,实际传入构造器的参数也是Integer类型,下面代码正确 new<Integer> Test(1); //显式指定构造器的T参数类型是String,实际传入构造器的参数是Inte类型,代码报错 new<String> Test(1); } }
在前面的博客里面我们介绍过java7新增的菱形语法,我们在调用构造器时构造器后可以使用一对尖括号来代表泛型信息,但是如果现在程序显式指定了泛型构造器中声明的类型参数的实际类型时,就不能使用菱形语法了呢。现在我们来看下面的代码:
public class Test<S> { //泛型构造器 public <T> Test(T name) { System.out.println(name.toString()); } public static void main(String[] args) throws Exception { //没有显式指定构造器类型参数类型,可以使用菱形语法 Test<String> test1 = new Test<>(1); //显式指定构造器类型参数类型,不可以再使用菱形语法,下面代码报错 Test<String> test2 = new<Integer> Test<>(1); //显式指定构造器类型参数类型,不可以再使用菱形语法,应该也同时指定菱形里面的构造器类型 Test<String> test3 = new<Integer> Test<String>(1); } }
- 5,泛型方法和泛型重载
public <T> String test(T name) { return ""; } public <T> void test(T name) { } public static void show(List<? extends Number> l){ } public static void show(List<? super String> l){ }
- 6,java8改进的类型推断
1),可通过调用方法的上下文来推断类型参数的目标类型
2),可在方法调用链中,将推断得到的类型参数传递给最后一个方法。
具体的我们看下面的代码,下面代码定义几个方法:
public class Test<S> { public static <T> Test<T> test() { return null; } public static <T> Test<T> test1(T head, Test<T> test) { return null; } S head() { return null; } public static void main(String[] args) throws Exception { Test<String> test1 = Test.test(); Test<String> test2 = Test.<String> test(); Test.test1(1, Test.test()); Test.test1("", Test.<String> test()); } }我们现在来研究下上面main方法里面的代码,第一行和第二行效果和作用完全一样,在第一行代码中我们无须显式指定类型参数为String,因为程序需要将该方法的返回值赋值给了Test<String>类型的test1变量,所以系统自动推断出了这里的类型参数T应该是String类型。第三行和第四行的效果和作用也完全一样,同样的我们在使用第3行代码的时候也无须显式指定test方法的类型参数T是Integer类型的,因为程序在调用test1这个方法的时候从第一个参数就可以推断出后面第2个参数的类型了,也就是后面的Test.test()这个test方法的类型参数T了呢。
当然系统的自动的推断泛型的能力也不是万能的,比如我们调用上面类的方法就需要强转:
String huhu = Test.test().head();除非我们这样子写:
String huhu = Test.<String> test().head();