9.4 泛型方法 9.4.1 定义泛型方法

9.4 泛型方法

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

9.4.1 定义泛型方法

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

1
2
3
4
5
6
7
static void fromArrayToCollection(Object[] a, Collection<object> c){
// 遍历a数组
for (object o: a){
// 把a数组中的每一个元素添加到c集合中
c.add(o);
}
}

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

1
2
3
4
5
6
// 定义String类型的数组
String[] strArr = {"a","b"};
// 创建泛型实参为String的List
List<String> strList= new ArrayList<>();
// Collection<String>对象不能当成Collection<String>使用,下面代码出现编译错误
fromArrayToCollection(strArr, strList);

可见上面fromArrayToCollection方法的参数类型不可以使用Collection<String>

那使用通配符Collection<?>是否可行呢?显然也不行,因为Java不允许把对象放进一个未知类型的集合中

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

泛型方法的语法格式

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

把上面方法的格式和普通方法的格式进行对比,不难发现泛型方法的方法签名比普通方法的方法签名多了泛型形参声明,

  • 泛型形参声明以英文尖括号<>括起来,
  • 多个泛型形参之间以硬逗号, 隔开,
  • 所有的泛型形参声明放在方法修饰符方法返回值类型之间

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

1
2
3
4
5
6
7
8
// 修饰符 泛型形参声明 返回值 方法名 方法参数列表
static <T> void fromArrayToCollection (T[] a, Collection<T> c){
// 遍历a数组
for (T o:a){
// 添加a数组中的元素到集合c中
c.add(o);
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.*;

public class GenericMethodTest {
// 声明一个泛型方法,该泛型方法中带一个T泛型形参,
// 修饰符static,泛型形参T,返回值void,方法名fromArrayToCollection
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}

public static void main(String[] args) {
// 定义一个Object类型的数组
Object[] objectArray = new Object[100];
// 定义一个保存Object元素的Collection
Collection<Object> objectCollection = new ArrayList<>();

// 实际参数1和实际参数2存放的都是Object的情况
// 下面代码中T代表Object类型
fromArrayToCollection(objectArray, objectCollection);

// 定义一个String类型的数组
String[] stringArray = new String[100];
// 定义一个String类型的Collection
Collection<String> stringCollection = new ArrayList<>();

// 实际参数1和实际参数2存放的都是String的情况
// 下面代码中T代表String类型
fromArrayToCollection(stringArray, stringCollection);

// 实际参数1存放的类型是String,实参2存放的是Object的情况。
// 下面代码中T代表Object类型
fromArrayToCollection(stringArray, objectCollection);

// 定义Integer的的数组
Integer[] integerArray = new Integer[100];
// 定义Float数组
Float[] floatArray = new Float[100];
// 定义Number数组
Number[] numberArray = new Number[100];
// 定义Number的Collection
Collection<Number> numberCollection = new ArrayList<>();

// 实参1存放的是Integer,实参2存放的是Number时
// 下面代码中T代表Number类型
fromArrayToCollection(integerArray, numberCollection);

// 实参1存放的是Float,实参2存放的是Number时
// 下面代码中T代表Number类型
fromArrayToCollection(floatArray, numberCollection);

// 实参1存放的是Number,实参2存放的时Number时
// 下面代码中T代表Number类型
fromArrayToCollection(numberArray, numberCollection);

// 实参1存放的是Number,实参2存放的是Object时
// 下面代码中T代表Object类型
fromArrayToCollection(numberArray, objectCollection);

// 下面代码中T代表String类型,但numberArray是一个Number数组,
// 因为Number既不是String类型,
// 也不是它的子类,所以出现编译错误
fromArrayToCollection(numberArray, stringCollection);
}
}

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

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

1
fromArrayToCollection(stringArray, stringCollection);

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

对于如下调用代码:

1
fromArrayToCollection(integerArray, numberCollection);

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import java.util.*;

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()方法,该方法用于将前一个集合里的元素复制到下一个集合中,该方法中的两个形参fromto的类型都是 Collections<T>,这要求调用该方法时的两个集合实参中的泛型类型相同,否则编译器无法准确地推断出泛型方法中泛型形参的类型。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.*;

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集合里元素类型的子类即可。

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