java里程碑之泛型--泛型方法

前面我已经介绍过了,我们可以在定义类和接口的时候使用类型形参,在该类的方法定义中,成员变量定义中,这些类型形参都可以被当成普通类型来使用。但是如果我们在定义类和接口的时候没有使用类型形参,但是在定义方法的时候想自己定义自己的类型形参,这样子也是可以的,这里也就是我们说的泛型方法。
想了解泛型方法,首先就要知道为什么会出现这种泛型方法的原因,我们先来考虑下面的情景。我们现在要实现这样一个方法,该方法负责将一个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,定义泛型方法
OK,现在就让我们使用泛型方法(generic method)吧,使用泛型方法可以很好的绕开这个问题。所谓的泛型方法就是申明方法定义的时候同时也定义了一个或者多个类型形参。具体语法如下:
修饰符 <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,调用泛型方法
与类和接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,编译器会根据实参推断类型参数的值,推断出最直接的类型参数。比如我们调用上面的方法,直接fromArrayToCollection(array2, new ArrayList<Number>())就可以。
//定义一个方法,将一个数组里面的元素复制到一个集合中
	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,泛型方法和类型通配符的区别

在大部分情况下都可以使用泛型方法来代替类型通配符。比如java中集合有2个方法:
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,菱形语法与泛型构造器
前面我们看到了泛型方法允许在方法签名中声明类型参数,java同时也允许在构造器签名中申明类型参数,这样就产生了所谓的泛型构造器。在实际的调用过程中,我们不仅可以让java根据参数的类型来推断类型参数的类型,也可以显式的为构造器的类型形参指定实际的类型。比如下面的代码:
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,泛型方法和泛型重载
在写泛型的方法时,要是只是泛型的类型参数不同,那么这2个方法就算是同一个方法。因为编译后的泛型就去掉了,所以当然是一个方法。但是我现在在用java1.8撸码,方法的返回值不同,编译也报错,说已经存在了同一个方法了,所以以后还是尽量绕开泛型方法的重载好了。比如下面的代码都报错:
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改进的类型推断
java8改进了泛型方法的类型推断能力,具体的要记住下面2句话:
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();

原文地址:https://www.cnblogs.com/LinkinPark/p/5232981.html