Java泛型应用



Java语言的泛型实现方式是擦拭法(Type Erasure)。

所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

擦拭法

所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类Pair<T>,这是编译器看到的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}

而虚拟机根本不知道泛型。这是虚拟机执行的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Pair {
private Object first;
private Object last;
public Pair(Object first, Object last) {
this.first = first;
this.last = last;
}
public Object getFirst() {
return first;
}
public Object getLast() {
return last;
}
}

因此,Java使用擦拭法实现泛型,导致了:

  • 编译器把类型<T>视为Object
  • 编译器根据<T>实现安全的强制转型。

而且,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

擦拭法决定了泛型<T>

  • 不能是基本类型,例如:int
  • 不能获取带泛型类型的Class,例如:Pair<String>.class
  • 不能判断带泛型类型的类型,例如:x instanceof Pair<String>
  • 不能实例化T类型,例如:new T()

通配符

extends通配符

如果我们考察对Pair<? extends Number>类型调用getFirst()方法,实际的方法签名变成了:

1
<? extends Number> getFirst();

即返回值是NumberNumber的子类,因此,可以安全赋值给Number类型的变量:

1
Number x = p.getFirst();

然后,我们不可预测实际类型就是Integer,例如,下面的代码是无法通过编译的:

1
Integer x = p.getFirst();

这是因为实际的返回类型可能是Integer,也可能是Double或者其他类型,编译器只能确定类型一定是Number的子类(包括Number类型本身),但具体类型无法确定。

有些童鞋会问了,既然p的定义是Pair<? extends Number>,那么setFirst(? extends Number)为什么不能传入Integer

原因还在于擦拭法。如果我们传入的pPair<Double>,显然它满足参数定义Pair<? extends Number>,然而,Pair<Double>setFirst()显然无法接受Integer类型。

这就是<? extends Number>通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number)

这里唯一的例外是可以给方法参数传入null

1
2
p.setFirst(null); // ok, 但是后面会抛出NullPointerException
p.getFirst().intValue(); // NullPointerException

小结:

使用类似<? extends Number>通配符作为方法参数时表示:

  • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();
  • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);

即一句话总结:使用extends通配符表示可以读,不能写。

使用类似<T extends Number>定义泛型类时表示:

  • 泛型类型限定为Number以及Number的子类。

super通配符

使用<? super Integer>通配符表示:

  • 允许调用set(? super Integer)方法传入Integer的引用;
  • 不允许调用get()方法获得Integer的引用。

唯一例外是可以获取Object的引用:Object o = p.getFirst()

换句话说,使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。

对比extends和super通配符

我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:

  • <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
  • <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

一个是允许读不允许写,另一个是允许写不允许读。

先记住上面的结论,我们来看Java标准库的Collections类定义的copy()方法:

1
2
3
4
5
6
7
8
9
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
}

它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。

这个copy()方法的定义就完美地展示了extendssuper的意图:

  • copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
  • copy()方法内部也不会修改src,因为不能调用src.add(T)

使用泛型抽离通用的方法

1、对于有共同的非Object父类

1
2
3
public static <T extends Student> void test(List<T> dataList) {  
……
}

这里的 T可以是Student的子类,我们可以通过T直接来操作对象数据

2、常规通用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static <T> void usersBlankNum(Class<T> clazz, T target) {
//将传过来的对象进行赋值处理,此时u可用来代表传过来的对象(本示意中是Users),
T cur = clazz.cast(target);
//以下是验证此示意中实体类可被操作了
//getDeclaredFields():获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的声明字段。
//.getClass()是一个对象实例的方法,只有对象实例才有这个方法,具体的类是没有的
for (Field field : cur.getClass().getDeclaredFields()) {
//允许获取实体类private的参数信息 field.setAccessible(true);
field.setAccessible(true);
try {
System.out.println(field.getName()+": "+String.valueOf(field.get(cur)));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

3、一个List to Array方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public static <T> T[] asArray(List<? extends T> list) {
T[] arr;
int size = 0;
Class clazz = Object.class;

if (!isNullOrEmpty(list)) {
clazz = list.get(0).getClass();
size = list.size();
}

arr = isNullOrEmpty(list) ? (T[]) Array.newInstance(clazz, size) : list.toArray((T[]) Array.newInstance(clazz, size));
return arr;
}

附录

相关泛型术语

  1)参数化的类型:List

  2)实际类型参数:String

  3)泛型:List

  4)形式类型参数:E

  5)无限制通配符类型:List<?>

  6)原生态类型:List

  7)递归类型限制:<T extends Comparable>

  8)有限制的通配符类型:List<? extends Number>

  9)泛型方法:static List union()

  10)类型令牌:String.class

常用的形式类型参数

  1)T 代表一般的任何类。

  2)E 代表 Element 的意思,或者 Exception 异常的意思。

  3)K 代表 Key 的意思。

  4)V 代表 Value 的意思,通常与 K 一起配合使用。

  5)S 代表 Subtype 的意思

参考文档

廖雪峰

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  1. © 2020-2021 Lauy    湘ICP备20003709号

请我喝杯咖啡吧~

支付宝
微信