8.4 泛型方法

Posted weststar

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了8.4 泛型方法相关的知识,希望对你有一定的参考价值。

一、定义泛型方法

所谓泛型方法,就是在声明方法时定义一个多个泛型形参。泛型方法的语法格式:

修饰符 <T,S> 返回值类型 方法名(形参列表)
{
    ///    方法体
}

泛型形参声明以尖括号括起来,多个泛型形参之间以逗号(,)隔开,所有泛型形参声明方法在修饰符和方法返回值类型之间
问题:将一个Object数组的所有元素添加到一个Collection集合中。

import java.util.*;
public class GenericMethodTest 
{
	//声明一个泛型方法,该泛型方法中带有一个T泛型形参
	static <T> void fromArrayToCollection(T[] a,Collection<T> c)
	{
		for(T o:a)
		{
			c.add(o);
		}
	}
	public static void main(String[] args) 
	{
		var oa=new Object[100];
		Collection<Object> co=new ArrayList<>();
		//下面代码的T代表Object类型
		fromArrayToCollection(oa,co);


		//下面代码的T代表String类型
		var sa=new String[100];
		Collection<String> cs=new ArrayList<>();
		fromArrayToCollection(sa,cs);
		
		//下面代码的T代表Object类型
		fromArrayToCollection(sa,co);

		var ia=new Integer[100];
		var fa=new Float[100];
		var na=new Number[100];
		Collection<Number> cn=new ArrayList<>();
		//下面代码T代表Number类型
		fromArrayToCollection(ia,cn);
		//下面代码T代表Number类型
		fromArrayToCollection(fa,cn);
		//下面代码T代表Number类型
		fromArrayToCollection(na,cn);

		//下面代码T代表Object类型
		fromArrayToCollection(na,co);

		//下面代码T代表String类型,但na是一个Number数组
		//因为Number既不是String类型,也不是它的子类,所以出现编译错误
		fromArrayToCollection(na,cs);
	}
}
---------- 编译Java ----------
GenericMethodTest.java:44: 错误: 无法将类 GenericMethodTest中的方法 fromArrayToCollection应用到给定类型;
		fromArrayToCollection(na,cs);
		^
  需要: T[],Collection<T>
  找到: Number[],Collection<String>
  原因: 推论变量 T 具有不兼容的上限
    等式约束条件:String
    下限:Number
  其中, T是类型变量:
    T扩展已在方法 <T>fromArrayToCollection(T[],Collection<T>)中声明的Object
1 个错误

输出完成 (耗时 4 秒) - 正常终止

与类、接口中使用的泛型参数不同的是,方法中的泛型参数无需显式地传入实际类型参数,如上面程序所示,当程序中调用fromArrayToCollection()方法时,无需传入在调用该方法前传入String、Object等类型,但系统依然可以知道为泛型实际的类型,因为编译器根据实参推断出泛型所代表的类型,它通常推断出最直接的类型。例如,下面调用代码:

fromArrayToCollection(sa,cs);

cs是一个Collection类型,与方法定义时的fromArrayToCollection(T[] a,Collection c)进行比较——只比较泛型参数,不难发现T类型代表的实际类型是String类型。
对于如下调用代码

fromArrayToCollection(ai,cn);

cn是一个Collection类型,与方法定义时的fromArrayToCollection(T[] a,Collection c)进行比较——只比较泛型参数,不难发现T类型代表的实际类型是Number类型,Integer是Number的子类,子类类型变量可以自动转化为父类类型变量。

为了编译器能够正确推断出泛型方法中泛型的类型,不要制造迷惑!一旦系统迷惑,将出现编译错误:

import java.util.*;
public class ErrorTest 
{
	//声明一个泛型方法,该泛型中带有一个T泛型形参
	static <T> void test(Collection<T> from,Collection<T> to)
		//将集合from的元素添加到集合to中
	{
		for(var ele:from)
		{
			to.add(ele);
		}
	}
	public static void main(String[] args) 
	{
		//当两个集合元素都相同时,系统可以推断出正确泛型类型
		List<Integer> ic1=new ArrayList<>();
		ic1.add(1);
		ic1.add(2);
		ic1.add(3);
		List<Integer> ic2=new ArrayList<>();
		test(ic1,ic2);//此时test()方法的泛型类型为Integer
		System.out.println(ic2);//[1,2,3]

		//当两个集合元素不同时,系统将迷惑
		List<Object> as=new ArrayList<>();
		ic1.add(1);
		ic1.add(2);
		ic1.add(3);
		List<String> ao=new ArrayList<>();
		test(as,ao);//此时无法判断test()方法的泛型类型
		System.out.println(ao);
	}
}
---------- 编译Java ----------
ErrorTest.java:21: 错误: 无法将类 ErrorTest中的方法 test应用到给定类型;
		test(ic1,ic2);//此时test()方法的泛型类型为Integer
		^
  需要: Collection<T>,Collection<T>
  找到: List<Integer>,List<Number>
  原因: 推论变量T具有不兼容的等式约束条件Number,Integer
  其中, T是类型变量:
    T扩展已在方法 <T>test(Collection<T>,Collection<T>)中声明的Object
ErrorTest.java:30: 错误: 无法将类 ErrorTest中的方法 test应用到给定类型;
		test(as,ao);//此时test()方法的泛型类型为Integer
		^
  需要: Collection<T>,Collection<T>
  找到: List<Object>,List<String>
  原因: 推论变量T具有不兼容的等式约束条件String,Object
  其中, T是类型变量:
    T扩展已在方法 <T>test(Collection<T>,Collection<T>)中声明的Object
2 个错误

输出完成 (耗时 1 秒) - 正常终止

上面程序传入test()方法的两个实际参数,其中as的数据类型是List,而ao的数据类型是List,与泛型方法签名进行对比:test(Collection from,Collection to),编译器无法识别T所代表的实际类型。可以将上面方法改为:

import java.util.*;
public class RightTest
{
	//声明一个泛型方法,该泛型方法中带有一个T形参
	static <T> void test(Collection<? extends T> from,Collection<T> to)
	{
		for(var ele:from)
		{
			to.add(ele);
		}
	}
	public static void main(String[] args)
	{
		List<Object> ao=new ArrayList<>();
		List<String> as=new ArrayList<>();
		as.add("good");
		as.add("night");
		test(as,ao);
		System.out.println(ao);//[good, night]
		System.out.println(ao.get(0).getClass());//class java.lang.String
	}
}

二、泛型方法和类型通配符的区别

大多数都可以使用泛型方法来代替类型通配符。例如Java的Collection接口中的两个方法:

public interface Collection<E>
{
    boolean containsAll(<?> c);
    boolean addAll(Collection<? extends E>);//设定上限的通配符 只进不出
}

上面的两个集合中的方法的形参都采用了类型通配符的形式,也可以采用泛型的形式,如下所示:

public interface Collection<E>
{
    <T> boolean containsAll(Collection<T> c);
    <T extends E> boolean  addAll(Colleaction<T> c);
}

上面两个泛型形参T只使用了一次,泛型形参T产生唯一的效果就是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符是设计用来支持灵活的子类化。
泛型方法允许泛型形参被用来表示一个或多个参数之间的类型依赖关系,或者返回值与参数之间的类型依赖关系。如果没有这种依赖关系就不应该使用泛型方法。
注:如果方法中一个形参(a)的类型或返回值的类型依赖于另一个形参(b)的类型,则形参(b)的类型声明就不应该使用通配符——因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果形参(b)的类型无法确定,程序就无法确定形参(a)的类型。在这种情况下,只能考虑在方法签名中声明泛型——也就是泛型放啊
同时使用泛型方法和类型通配符:

public class Collection
{
    public static <T> void copy(List<T> dest,List<? extends T> src);
}

上面方法签名也可以改为使用泛型方法,不使用类型通配符,如下所示:

public class Collection
{
    public static <T,S extends T> void copy(List<T> dest,List<S> src);
}

该方法签名中泛型形参S只使用了一次,且没有其他参数类型、返回值类型依赖它,那么泛型形参S就没有存在的必要,即可以通过通配符来代替S。使用通配符比使用泛型方法更加清晰和准确。

三、"菱形"语法与泛型构造器

Java允许在构造器的签名中声明泛型形参,这就产生所谓的泛型构造器
一旦定义了泛型构造器,接下来在调用构造器时,不仅可以让Java根据数据参数的类型来“推断”泛型形参的类型,而且程序员可以显式地为构造器中泛型形参指定实际的类型。

class Foo 
{
	public <T> Foo(T t)
	{
		System.out.println(t);
		System.out.println(t.getClass());
	}
}
public class GenericConstructor
{
	public static void main(String[] args)
	{
		//定义构造器中的T类型为String
		new Foo("疯狂Java讲义");
		//泛型构造器中T类型为Integer
		new Foo(200);

		//显式指定泛型构造器中的T类型为String
		new <String> Foo("疯狂Andoird讲义");

		//下面代码竟出现错误,指定String类型,传入的参数为Double类型
		// 错误: 不兼容的类型: double无法转换为String
		//new <String> Foo(12.3);
	}
}
---------- 运行Java捕获输出窗 ----------
疯狂Java讲义
class java.lang.String
200
class java.lang.Integer
疯狂Andoird讲义
class java.lang.String

输出完成 (耗时 0 秒) - 正常终止

java 7新增了“菱形”语法,它允许在调用构造器时在构造器后使用一对尖括号来代表泛型信息,;但如果程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。

class MyClass<E>
{
	public <T> MyClass(T t)
	{
		System.out.println("t参数的值为:" + t);
	}
}
public class GenericDiamondTest
{
	public static void main(String[] args)
	{
		// MyClass类声明中的E形参是String类型。
		// 泛型构造器中声明的T形参是Integer类型
		MyClass<String> mc1 = new MyClass<>(5);
		// 显式指定泛型构造器中声明的T形参是Integer类型,
		MyClass<String> mc2 = new <Integer> MyClass<String>(5);
		// MyClass类声明中的E形参是String类型。
		// 如果显式指定泛型构造器中声明的T形参是Integer类型
		// 此时就不能使用"菱形"语法,下面代码是错的。
//		MyClass<String> mc3 = new <Integer> MyClass<>(5);
	}
}
---------- 运行Java捕获输出窗 ----------
t参数的值为:5
t参数的值为:5

输出完成 (耗时 0 秒) - 正常终止

四、泛型方法与方法重载

方法重载实例:

public class MyUtils
{
    public static <T> void copy(Collection<T> dest,Collection<? extends T> src);
    public static <T> T void copy(Collection<? super T> dest,Collection<T> src);
}

上面的MyUtils类中包含了两个copy()方法,这两个方法的参数都是Collection对象,前一个集合里的元素是后一个集合元素的父类,但如果只是在该类中定义着两个方法不会有任何错误,但如果只要调用这个方法就会引起编译错误。例如:

List<Number> ln=new ArrayList<>();
List<Integer> li=new ArrayList<>();
MyUtils.copy(ln,li);

上面代码调用copy()方法,既可以匹配匹配第一个copy()方法,此时泛型T代表Number;也可以匹配第二个方法,此时泛型T代表的类型Integer。编译器无法确定想调用哪一个copy()方法,所以这行代码将出现编译错误。

五、类型推断

Java 8改进泛型方法的推断能力,类型推断主要有两个方法:
1、可以通过调用方法的上下文来推断的目标类型
2、可在方法调用链中,将推断的泛型传递给最后一个方法。

class MyUtil<E>
{
	public static <Z> MyUtil<Z> nil()
	{
		return null;
	}
	public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail)
	{
		return null;
	}
	E head()
	{
		return null;
	}
}
public class InferenceTest
{
	public static void main(String[] args)
	{
		// 可以通过方法赋值的目标参数来推断类型参数为String
		MyUtil<String> ls = MyUtil.nil();
		// 无需使用下面语句在调用nil()方法时指定类型参数的类型
		MyUtil<String> mu = MyUtil.<String>nil();
		// 可调用cons方法所需的参数类型来推断类型参数为Integer
		MyUtil.cons(42, MyUtil.nil());
		// 无需使用下面语句在调用nil()方法时指定类型参数的类型
		MyUtil.cons(42, MyUtil.<Integer>nil());

		// 希望系统能推断出调用nil()方法类型参数为String类型,
		// 但实际上Java 8依然推断不出来,所以下面代码报错
//		String s = MyUtil.nil().head();
		String s = MyUtil.<String>nil().head();
	}
}

以上是关于8.4 泛型方法的主要内容,如果未能解决你的问题,请参考以下文章

操作 Java 泛型:泛型在继承方面体现与通配符使用

这个嵌套类构造函数片段可以应用于泛型类吗?

8.4 类的重写

CentOS 8.4安装Docker

作业09-集合与泛型

Java泛型:类型擦除

(c)2006-2024 SYSTEM All Rights Reserved IT常识