说到泛型,大家肯定不会陌生。这个从Java SE5引入的新特性为我们的开发提供了极大地方便。一方面,使得我们可以对于不同数据类型的通用操作统一放在一个类或方法中去定义、实现,而无需针对每种不同的数据类型去提供多个类或方法;另一方面,泛型编程实现了编译期的类型安全检查,以避免运行期发生类型转换异常
泛型的基本使用
泛型类
JDK中大量应用了泛型类,比如我们经常使用的LinkedList、HashMap等。其实定义一个泛型类很简单,只需在类名后声明类型变量,多个类型变量之间使用,逗号分隔,并用<>尖括号括起来即可。一般地类型变量使用大写字母表示。通常,任意类型一般可命名为T,集合元素的类型常用E表示,映射的键、值类型常用K、V命名
这里我们定义了一个泛型类Pair,我们可以直接在成员变量及方法中使用泛型类的类型变量T
1/** 2 * 泛型类示例 3 * @param 4 * @apiNote T用来表示类型变量 5 */
6@ToString
7public class Pair<T> {
8 private T first;
9 private T second;
10
11 public Pair() {
12 }
13
14 public Pair(T first, T second){
15 this.first = first;
16 this.second = second;
17 }
18
19 public void setFirst(T first) {
20 this.first = first;
21 }
22
23 public void setSecond(T second) {
24 this.second = second;
25 }
26
27 public T getFirst() {
28 return first;
29 }
30
31
32 public T getSecond() {
33 return second;
34 }
35}
使用泛型类也很简单
1// 测试泛型类
2public static void testPair1() {
3 Pair pair = new Pair<>(); 4 pair.setFirst("Bob"); 5 pair.setSecond("Ed"); 6 7 String first = pair.getFirst(); 8 String second = pair.getSecond(); 910 System.out.println("pair1: " + pair);11 System.out.println("first: " + first);12 System.out.println("second: " + second);13}
测试结果如下
泛型接口
在JDK中大量使用了泛型接口,比如我们经常使用的List、Map等。定义泛型接口与泛型类没有太大区别,只是当其他类、接口在实现、继承泛型接口时,如果没有明确泛型接口类型变量的具体参数,则其也必须一并声明定义类型变量。这里我们定义了一个泛型接口CRUDService
1/** 2 * 泛型接口 3 * @param 类型变量 4 */
5public interface CRUDService<T> {
6
7 int add(T record);
8
9 int update(T record);
10
11 T find(int id);
12
13 int delete(int id);
14}
这里PersonService类实现了CRUDService接口,同时指定类型变量T的类型为Person
1public class PersonService implements CRUDService<Person> {
2
3 @Override
4 public int add(Person record) {
5 return 1;
6 }
7
8 @Override
9 public int update(Person record) {
10 return 1;
11 }
12
13 @Override
14 public Person find(int id) {
15 return null;
16 }
17
18 @Override
19 public int delete(int id) {
20 return 1;
21 }
22}
23
24@Data
25class Person {
26 private String name;
27 private Integer id;
28}
而如果没有明确类型变量T,则CommonService类依然还需要继续声明类型变量
1public class CommonService<T> implements CRUDService<T> {
2 @Override
3 public int add(T record) {
4 return 1;
5 }
6
7 @Override
8 public int update(T record) {
9 return 1;
10 }
11
12 @Override
13 public T find(int id) {
14 return null;
15 }
16
17 @Override
18 public int delete(int id) {
19 return 1;
20 }
21}
泛型方法
泛型方法不仅可以在泛型类中定义,也可以在普通类中定义。这里我们依然在Pair泛型类中来展示如何定义泛型方法。如下所示,泛型方法可以是静态的(get),也可以是非静态的(getInfo)。对于泛型方法而言,其必须要声明类型变量 ,且位于方法修饰符(public、public static)与方法返回值之间。所以Pair中的setFirst方法并不是泛型方法,其虽然使用了泛型类所声明的类型变量T,但是其并没有声明类型变量,所以只是Pair类中一个普通的成员方法。同样地,对于泛型方法而言,其不仅可以使用泛型类所声明的类型变量,同时还可以使用其自身声明的类型变量。值得一提的是,当 泛型类所声明的类型变量 与 泛型方法所声明的类型变量 重名时,即这里都是T,在该泛型方法中的重名的类型变量均是指泛型方法的类型变量
1/** 2 * 泛型类示例 3 * @param 4 * @apiNote T用来表示类型变量 5 */
6@ToString
7public class Pair<T> {
8 private T first;
9 private T second;
10
11 /**12 * @apiNote 其不是泛型方法13 */
14 public void setFirst(T first) {
15 this.first = first;
16 }
17
18 ...
19
20 /**21 * 泛型方法1 : 获取数组中的第1个元素22 * @apiNote 泛型方法的类型参数T 与 泛型类的类型参数T 重名,故该方法中的T均指的是泛型方法的类型参数23 * @param array24 * @param 25 * @return26 */
27 public static T get(T... array) {28 return array[0];29 }3031 /**32 * 泛型方法2 : 打印实参33 * @param e34 * @param 35 */36 public void getInfo(T e) {37 System.out.println("getInfo: " + e);38 }39}
现在让我们来测试下如何使用泛型方法
1// 测试泛型方法
2public static void testPair2() {
3 System.out.println("-------- Test: getInfo Method --------");
4 Pair pair1 = new Pair<>(); 5 pair1.setFirst("First"); 6 pair1.setSecond("Second"); 7 System.out.println("pair1: " + pair1); 8 // 类型变量T的类型信息可省略,编译器可自行推断出类型信息 9 pair1.getInfo(1.234);10 pair1.getInfo("Bob");11 pair1.getInfo(true);1213 System.out.println("-------- Test: get Method --------");14 // 类型变量T的类型信息可省略,编译器可自行推断出类型信息15 Integer num1 = Pair.get(3,0,1,2);16 Integer num2 = Pair.get(1,2,3,4);17 System.out.println("num1: " + num1 + " num2: " + num2);18}
测试结果如下
Note
在一个泛型类的非静态泛型方法中,如果该泛型方法没有使用泛型类的泛型参数,则该泛型方法在使用上有一些特别注意的地方。如下面的echo方法
1public class Pair<T> {
2 private T first;
3 private T second;
4
5 /* 6 * 不使用泛型类的类型变量的非静态泛型方法 7 */
8 public R echo(R r) { 9 return r;10 }11 ...12}
在我们通过一个Pair实例调用echo泛型方法时,该Pair实例的类型必须是泛型类型,即使我们在该方法中用不上泛型类的类型变量;否则出于对向下兼容的妥协,Java编译器(JDK8)会将该泛型方法处理为一个普通方法,即使显式地设置泛型方法的类型变量也无效。下面即是一个该Feature测试用例
1public static void testEcho() {
2 // 实例变量pair1类型 不使用泛型而使用原始类型
3 // 则泛型方法的类型变量将会被擦去,无法进行类型推断,即退化为普通方法
4 // 故必须显式地强制转换
5 Pair pair1 = new Pair();
6 Object o1 = pair1.echo("Bob");
7 Double num1 = (Double)pair1.echo(6.66);
8 System.out.println("-------- Demo1 --------");
9 System.out.println("o1: " + o1);
10 System.out.println("num1: " + num1);
11
12
13 // 实例变量pair2使用泛型类型, 则泛型方法的类型变量可以自动进行推断
14 Pair> pair2 = new Pair();
15 String str2 = pair2.echo("Aaron");
16 Double num2 = pair2.echo(1.234);
17 System.out.println("-------- Demo2 --------");
18 System.out.println("str2: " + str2);
19 System.out.println("num2: " + num2);
20}
测试结果如下
类型限定
很多时候,我们需要能够对类型变量进行一定的约束限定。这里我们以泛型方法的类型变量限定为例进行介绍,泛型类的类型变量的限定与之一致。我们向Pair类中添加了一个新的泛型方法minmax,该方法内部会调用类型为T的temp对象的compareTo方法进行比较。那么问题来了,如何保证类型T中存在compareTo方法呢?我们知道compareTo是由Comparable接口定义的一个方法,如果类型T实现了Comparable接口,则一定可以正常调用compareTo方法。所以这里我们通过extends来限定类型变量T必须实现Comparable接口,即要求类型T必须是Comparable的子类型
1/** 2 * 泛型类示例 3 * @param 4 * @apiNote T用来表示类型变量 5 */
6public class Pair<T> {
7
8 /** 9 * 泛型方法3: 限定的泛型方法10 * @param array11 * @param 12 * @return13 */
14 public static Pair minmax(T... array) {15 if( array==null || array.length==0 ) {16 return null;17 }1819 T min = array[0];20 T max = array[1];21 for(int i=1; i22 T temp = array[i];23 min = temp.compareTo(min)>0 ? min : temp;24 max = temp.compareTo(max)>0 ? temp : max;25 }26 return new Pair<>(min, max);27 }28}
具体地,我们可以在类型变量的声明处施加多个类型限定,限定类型之间使用&进行分隔。其中接口限定的数量没有限制。而类限定最多只能有一个,且必须位于限定类型列表的最前面。语法实例如下所示
1// 类型变量T 由两个接口类型进行限定
2T extends Comparable & Serializable
3// 类型变量T 由一个类、两个接口进行限定, 且类ArrayList必须位于最前面
4T extends ArrayList & Comparable & Serializable
现在来测试下我们新加入的限定的泛型方法
1// 测试限定的泛型方法
2public static void testPair3() {
3 Pair pair = Pair.minmax(4,1,3,8,6);4 System.out.println("pair: " + pair);5}
测试结果如下
参考文献
Java核心技术·卷I 凯.S.霍斯特曼著