Java 学习之路 之 泛型方法(四十二)

前面介绍了在定义类、接口时可以使用类型形参,在该类的方法定义和 Field 定义、接口的方法定义中,这些类型形参可被当成普通类型来用。在另外一些情况下,我们定义类、接口时没有使用类型形参,但定义方法时想自己定义类型形参,这也是可以的,Java 5 还提供了对泛型方法的支持。

1,定义泛型方法

假设需要实现这样一个方法----该方法负责将一个 Object 数组的所有元素添加到一个 Collection 集合中。考虑采用如下代码来实现该方法。

static void fromArrayToCollection(Object[] a, Collection<Object> c){
  for (Object o : a){
    c.add(o);
  }
}

上面定义的方法没有任何问题,关键在于方法中的 c 形参,它的数据类型是 Collection<Object>。正如前面所介绍的,Collection<String> 不是 Collection<Object> 的子类型——所以这个方法的功能非常有限,它只能将 Object 数组的元素复制到 Object(Object的子类不行)Collection 集合中,即下面代码将引起编译错误。

String[] strArr = {"a", "b"};
List<String> strList = new ArrayList<>();
// Collection<String> 对象不能当成 Collection<Object> 使用,下面代码出现编译错误
fromArrayToCollection(strArr, strList);

可见上面方法的参数类型不可以使用 Collection<String>,那使用通配符 Collection<?>是否可行呢?显然也不行,我们不能把对象放进一个未知类型的集合中。

为了解决这个问题,可以使用 Java 5 提供的泛型方法(Generic Method)。所谓泛型方法,就是在声明方法时定义一个或多个类型形参。泛型方法的用法格式如下:

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

把上面方法的格式和普通方法的格式进行对比,不难发现泛型方法的方法签名比普通方法的方法签名多了类型形参声明,类型形参声明以尖括号括起来,多个类型形参之间以逗号(,)隔开,所有的类型形参声明放在方法修饰符和方法返回值类型之间。

采用支持泛型的方法,就可以将上面的 fromArrayToCollection 方法改为如下形式:

static <T> void fromArrayToCollection(T[] a, Collection<T> c){
	for(T o: a){
		c.add(o);
	}
}

下面程序示范了完整的用法。

package com.sym.demo4;

import java.util.ArrayList;
import java.util.Collection;

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) {
		Object[] oa = new Object[100];
		Collection<Object> co = new ArrayList<>();
		// 下面代码中 T 代表 Object 类型
		fromArrayToCollection(oa, co);
		String[] sa = new String[100];
		Collection<String> cs = new ArrayList<>();
		// 下面代码中 T 代表 String 类型
		fromArrayToCollection(sa, cs);
		// 下面代码中 T 代表 Object 类型
		fromArrayToCollection(sa, co);
		Integer[] ia = new Integer[100];
		Float[] fa = new Float[100];
		Number[] 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);
	}
}

上面程序定义了一个泛型方法,该泛型方法中定义了一个 T 类型形参,这个 T 类型形参就可以在该方法内当成普通类型使用。与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。

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

fromArrayToCollection(sa, cs);

上面代码中 cs 是一个 Collection<String> 类型,与方法定义时的 fromArrayToCollection(T[] a, Collection<T> c) 进行比较——只比较泛型参数,不难发现该T类型形参代表的实际类型是 String 类型。

对于如下调用代码:

fromArrayToCollection(ia, cn);

上面的 cn 是 Collection<Number> 类型,与此方法的方法签名进行比较——只比较泛型参数,不难发现该 T 类型形参代表了 Number 类型。

为了让编译器能准确地推断出泛型方法中类型形参的类型,不要制造迷惑!系统一旦迷惑了,就是你错了!看如下程序。

package com.sym.demo4;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class ErrorTest {
	// 声明一个泛型方法,该泛型方法中带一个 T 类型形参
	static <T> void test(Collection<T> from, Collection<T> to){
		for(T ele : from){
			to.add(ele);
		}
	}
	public static void main(String[] args) {
		List<Object> as = new ArrayList<>();
		List<String> ao = new ArrayList<>();
		// 下面代码将引起编译错误
		test(as, ao);
	}
}

上面程序中定义了 test 方法,该方法用于将前一个集合里的元素复制到下一个集合中,该方法中的两个形参 from、to 的类型都是 Collection<T>,这要求调用该方法时的两个集合实参中的泛型类型相同,否则编译器无法准确地推断出泛型方法中类型形参的类型。

上面程序中调用 test 方法传入了两个实际参数,其中 as 的数据类型是 List<String>,而 ao 的数据类型是 List<Object>,与泛型方法签名进行对比:test(Collection<T>a, Collection<T> c).编译器无法正确识别 T 所代表的实际类型。为了避免这种错误,可以将该方法改为如下形式:

package com.sym.demo4;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class RightTest {
	// 声明一个泛型方法,该泛型方法中带一个 T 类型形参
	static <T> void test(Collection<? extends T> from, Collection<T> to){
		for(T ele : from){
			to.add(ele);
		}
	}
	public static void main(String[] args) {
		List<Object> ao = new ArrayList<>();
		List<String> as = new ArrayList<>();
		// 下面代码完全正常
		test(as, ao);
	}
}

上面代码改变了 test 方法签名,将该方法的前一个形参类型改为 Collection<? extends T>,这种采用类型通配符的表示方式,只要 test 方法的前一个 Collection 集合里的元素类型是后一个 Collection 集合里元素类型的子类即可。

那么这里产生了一个问题:到底何时使用泛型方法?何时使用类型通配符呢?下面将具体介绍泛型方法和类型通配符的区别。

2,泛型方法和类型通配符的区别

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

public interface Collection<E>{
  boolean containAll(Collection<?> c);
  boolean addAll(Collection<? extends E> c);
  ...
}

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

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

上面方法使用了 <T extends E> 泛型形式,这时定义类型形参时设定上限(其中 E 是 Collection 接口里定义的类型形参,在该接口里E可当成普通类型使用)。

上面两个方法中类型形参 T 只使用了一次,类型形参 T 产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。

泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

如果某个方法中一个形参(a)的类型或返回值的类型依赖于另一个形参(b)的类型,则形参(b)的类型声明不应该使用通配符----因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果形参(b)的类型无法确定,程序就无法定义形参(a)的类型。在这种情况下,只能考虑使用在方法签名中声明类型形参——也就是泛型方法。

如果有需要,我们可以同时使用泛型方法和通配符,如 Java 的 Collections.copy()方法。

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

上面 copy 方法中的 dest 和 src 存在明显的依赖关系,从源 List 中复制出来的元素,必须可以“丢进”目标 List 中,所以源 List 集合元素的类型只能是目标集合元素的类型的子类型或者它本身。但 JDK 定义 src 形参类型时使用的是类型通配符,而不是泛型方法。这是因为:该方法无须向 src 集合中添加元素,也无须修改 src 集合里的元素,所以可以使用类型通配符,不使用泛型方法。

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

class collections{
  public static <T, S extends T> void copy(List<T> dest, List<S> scr){...}
  ...
}

这个方法签名可以代替前面的方法签名。但注意上面类型形参 S 它仅使甩了一次,没有其他参数的类型、方法返回值的类犁依赖于它,那类型形参 S 就没有存在的必要,即可以用通配符来代替 S 。使用通配符比使用泛型方法(在方法签名中显式声明类型形参)更加清晰和准确,因此 Java 设计该方法时采用了通配符,而不是泛型方法。

类型通配符与泛型方法(在方法签名中显式声明类型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显式声明。

3,Java 7 的“菱形”语法与泛型构造器

正如泛型方法允许在方法签名中声明类型形参一样,Java 也允许在构造器签名中声明类型形参,这样就产生了所渭的泛型构造器。

一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让 Java 根据数据参数的类型来“推断”类型形参的类型,而且程序员也可以显式地为构造器中的类型形参指定实际的类型。如下程序所示。

package com.sym.demo4;

class Foo{
	public <T> Foo(T t){
		System.out.println(t);
	}
}

public class GenericConstructor {
	public static void main(String[] args) {
		// 泛型构造器中的 T 参数为 String
		new Foo("疯狂 Java 讲义");
		// 泛型构造器中的 T 参数为 Integer
		new Foo(200);
		// 显式指定泛型构造器中的 T 参数为 String
		// 传给 Foo 构造器的实参也是 String 对象,完全正确
		new <String> Foo("疯狂 Android 讲义");//1
		// 显式指定泛型构造器中的 T 参数为 String
		// 传给 Foo 构造器的实参也是 Double 对象,下面代码出错
		new <String> Foo(12.3);//2
	}
}

上面程序中①号代码不仅显式指定了泛型构造器中的类型形参 T 的类型应该是 String,而且程序传给该构造器的参数值也是 String 类型,因此程序完至正常。但在②号代码处,程序显式指定了泛型构造器中的类型形参 T 的类型应该是 String,但实际传给该构造器的参数值是 Double 类型,因此这行代码,将会出现错误。

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

package com.sym.demo4;


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);
	}
}

上面程序中最后一行代码既指定了泛型构造器中的类型形参是 Integer 类型,又想使用“菱形”语法,所以这行代码无法通过编译。

4,设定通配符下限

假设自己实现一个工具方法:实现将 src 集合里的元素复制到 dest 集合里的功能,因为 dest 集合可以保存 src 集合里的所有元素,所以 dest 集合元素的类型应该是 src 集合元素类型的父类。为了表示两个参数之间的类型依赖,考虑同时使用通配符、泛型参数来实现该方法。代码如下:

public static <T> void copy(Collection<T> dest, Collection<? extends T> src){
  for( T ele : src){
      dest.add(ele);
  }
}

上面方法实现了前面的功能。现在假设该方法需要一个返回值,返回最后一个被复制的元素,则可以把上面方法改为如下形式:

public static <T> T copy(Collection<T> dest, Collection<? extends T> src){
  T last = null;
  for (T ele : src){
    last = ele;
    dest.add(ele);
  }
  return last;
}

表面上看起来,上面方法实现了这个功能,实际上有一个问题:当遍历 src 集合的元素时,src 元素的类型是不确定的(但可以肯定它是 T 的子类),程序只能用 T 来笼统地表示各种 src 集合的元素类型。例如如下代码:

List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
// 下面代码引起编译错误
Integer last = copy(ln, li);

上面代码中 ln 的类型是 List<Number>,与 copy 方法签名的形参类型进行对比即得到 T 的实际类型是 Number,而不是 Integer 类型——即 copy 方法的返回值也是 Number 类型,而不是 Integer 类型,但实际上是后一个复制元素的元素类型一定是 Integer。也就是说,程序在复制集合元素的过程中,丢失了 src 集合元素的类型。

对于上面的 copy 方法,可以这样理解两个集合参数之间的依赖关系:不管 src 集合元素的类型是什么,只要 dest 集合元素的类型与前者相同或是前者的父类即可。为了表达这种约束关系,Java 允许设定通配符的下限:<? super Type>,这个通配符表示它必须是 Type 本身,或是 Type 的父类。下面程序采用设定通配符下限的方式改写了前面的 copy 方法。

package com.sym.demo4;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class MyUtils {
	// 下面 dest 集合元素的类型必须与 src 集合元素的类型相同,或是其父类
	public static <T> T copy(Collection<? super T> dest, Collection<T> src){
		T last = null;
		for (T ele : src){
			last = ele;
			dest.add(ele);
		}
		return last;
	}
	public static void main(String[] args) {
		List<Number> ln = new ArrayList<>();
		List<Integer> li = new ArrayList<>();
		li.add(5);
		// 此处可准确地知道最后一个被复制的元素是 Integer 类型
		// 与 src 集合元素的类型相同
		Integer last = copy(ln, li);//1
		System.out.println(ln);
	}
}

使用这种语句,就可以保证程序的①处调用后推断出晟后一个被复制的元素类型是 Integer,而不是笼统的 Number 类型。

实际上,Java 集合框架中的 TreeSet<E> 自一个构造器也用到了这种设定通配符下限的语法,如下所示。

// 下面的 E 是定义 TreeSet 类时的类型形参
TreeSet(Comparator<? super E> c);

正如前章所介绍的,TreeSet 会对集合中的元素按自然顺序或定制顺序进行排序。如果需要 TreeSet 对集合中的所有元索进行定制排序,则要求 TreeSet 对象有一个与之关联的 Comparator 对象。上面构造器中的参数 c 就是进行定制排序的 Comparator 对象。

Comparator接U也是个带泛型声明的接口:

public interface Comparator<T>{
  int compare(T fst, T snd);
}

通过这种带下限的通配符的语法,可以在创建 TreeSet 对象时灵活地选择合适的 Comparator。假定需要创建一个 TreeSet<String> 集台,并传人一个可以比较 String 大小的Comparator,这个 Comparator 既可以是 Comparator<String>,也可以是 Comparator<Object> ----只要尖括号里传人的类型是 String 的父类型(或它本身)即可。如下程程序所示。

package com.sym.demo4;

import java.util.Comparator;
import java.util.TreeSet;

public class TreeSetTest {
	public static void main(String[] args) {
		// Comparator 的实际类型是 TreeSet 里实际类型的父类,满足要求
		TreeSet<String> ts1 = new TreeSet<>(
			new Comparator<Object>() {
				public int compare(Object fst, Object snd){
					return hashCode() > snd.hashCode() ? 1 : hashCode() < snd.hashCode() ? -1 : 0;
				}
			}
		);
		ts1.add("hello");
		ts1.add("wa");
		TreeSet<String> ts2 = new TreeSet<>(
			new Comparator<String>() {

				@Override
				public int compare(String first, String second) {
					return first.length() > second.length() ? -1 : first.length() < second.length() ? 1 : 0;
				}
			}
		);
		
		ts2.add("hello");
		ts2.add("wa");
		System.out.println(ts1);
		System.out.println(ts2);
	}
}

通过使用这种通配符下限的方式来定义 TreeSet 构造器的参数,就可以将所有可用的 Comparator 作为参数传入,从而增加了程序的灵活性。当然,不仅 TreeSet 有这种用法,TreeMap 也有类似的用法,具体请查阅 Java 的 API 文档。

5,泛型方法与方法重载

因为泛型既允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法定义。

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

上面的 MyUtils 类中包含两个 copy 方法,这两个方法的参数列表存在一定的区别,但这种区别不是很明确:这两个方法的两个参数都是 Collection 对象,前一个集合里的集合元素类型是后一个集合里集合元素类型的父类。如果这个类仅包含这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。例如,对于如下代码:

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

上面程序中粗体字部分调用 copy 方法,但这个 copy 方法既可以匹配①号 copy 方法,此时 T 类型参数的类型是 Number:也可以匹配②号 copy 方法,此时 T 参数的类型是Integer。编译器无法确定这行代码想调用哪个 copy 方法,所以这行代码将引起编译错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值