Java中的泛型带来了什么好处?规约。就像接口定义一样,可以帮助对于泛型类型和对象的使用上,保证类型的正确性。如果没有泛型的约束,程序员大概需要在代码里面使用大量的类型强制转换语句,而且需要非常清楚没有标注的对象实际类型,这是容易出错的、恼人的。但是话说回来,泛型可不只有规约,还有很多有趣的用法,容我一一道来。
泛型擦除
Java的泛型在编译阶段实际上就已经被擦除了(这也是它和C#泛型最本质的区别),也就是说,对于使用泛型的定义,对于编译执行的过程,并没有任何的帮助(有谁能告诉我为什么按着泛型擦除来设计?)。所以,单纯利用泛型的不同来设计接口,会遇到预期之外的问题,比如说:
1
2
3
4
|
public
interface
Builder
public
void
add(List
public
void
add(List
}
|
想这样设计接口?仅仅靠泛型类型的不同来设计重载接口?那是痴人说梦。但是如果代码变成这样呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
class
GenericTypes {
public
static
String method(List
System.out.println(
"invoke method(List
);
return
""
;
}
public
static
int
method(List
System.out.println(
"invoke method(List
);
return
1
;
}
public
static
void
main(String[] args) {
method(
new
ArrayList
method(
new
ArrayList
}
}
|
这个情况就有点特殊了,Sun的Javac编译器居然可以通过编译,而其它不行,这个例子来自IcyFenix的文章,有兴趣不妨移步参阅IcyFenix的文章以及下面的讨论。
方法泛型
在JDK的java.util.List接口里面,定义了这样一个方法:
1
2
3
|
public
interface
List
extends
Collection
}
|
事实上,这个方法泛型T表示的是任意类型,它可是和此例中的接口/类泛型E毫不相干啊。
如果我去设计方法,我可能写成这样:
1
|
|
其实这个T[ ] a参数的作用也容易理解:
- 确定了数组类型;
- 如果给定的数组a能够容纳得下结果,就会把结果放进a里面(JDK的注释有说明“If the list fits in the specified array, it is returned therein.”),同时也把a返回。
如果没有这个T[ ] a参数的话,光光定义一个方法泛型
1
2
3
4
5
6
7
8
9
|
public
class
Builder {
public
return
null
;
}
public
static
void
main(String[] args) {
String s =
new
Builder().call();
// ①
Integer i =
new
Builder().call();
// ②
new
Builder().
// ③
|
1
2
|
}
}
|
可以看到,call()方法返回的是类型E,这个E其实没有任何约束,它可以表示任何对象,但是代码上不需要强制转换就可以赋给String类型的对象s(①),也可以赋给Integer的对象i(②),甚至,你可以主动告知编译器返回的对象类型(③)。
链式调用
看看如下示例代码:
1
2
3
4
5
6
7
8
9
|
public
class
Builder
public
// 省略实现
}
public
static
void
main(String[] args) {
new
Builder
"3"
,
3
).change(
3
,
3
.0f).change(
3
.0f,
3
.0d);
}
}
|
同样一个change方法,接收的参数变来变去的,上例中方法参数从String-int变到int-float,再变为float-double,这样的泛型魔法在设计链式调用的方法的时候,特别是定义DSL语法的时候特别有用。
使用问号
其实问号帮助表示的是“通配符类型”,通配符类型 List> 与原始类型 List 和具体类型 List
来看一段有趣的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class
Wrapper
private
E e;
public
void
put(E e) {
this
.e = e;
}
public
E get(){
return
e;
}
}
public
class
Builder {
public
void
check(Wrapper> wrapper){
System.out.println(wrapper.get());
// ①
wrapper.put(
new
Object());
// ② wrong!
wrapper.put(wrapper.get());
// ③ wrong!
wrapper.put(
null
);
// ④ right!
}
}
|
Wrapper的类定义里面指定了它包装了一个类型为E的对象,但是在另一个使用它的类Builder里面,指定了Wrapper的泛型参数是?,这就意味着这个被包装的对象的类型是完全不可知的:
- 现在我可以调用Wrapper的get方法把对象取出来看看(①),
- 但是我不能放任意类型确定的对象进去,Object也不行(②),
- 即便是从wrapper里面get出来也不行(编译器太不聪明了是吧?③)
- 可奇葩的是,放一个null是可以被允许的,因为null根本就不是任何一个类型的对象(④,注意,不能放int这类的原语类型,虽然它不是对象,但因为它会被自动装箱成Integer,从而变成具体类型,所以是会失败的)。
现在思考一下,如果要表示这个未知对象是某个类的子类,上面代码的Wrapper定义不变,但是check方法写成:
1
|
public
void
check(Wrapper
extends
String> wrapper){
|
1
|
wrapper.put(
new
String());
|
1
|
}
|
这样呢?
……
依然报错,因为new String()确实是String的子类(或它自己)的对象,一点都没错,但是它可不见得和同为String子类(或它自己)的“?”属于同一个类型啊!
如果写成这样呢(注意extends变成了super)?
1
|
public
void
check(Wrapper
super
String> wrapper){
|
1
|
wrapper.put(
new
String());
|
1
|
}
|
这次对了,为什么呢?
……
因为wrapper要求put的参数“?”必须是String的父类(或它自己),而不管这个类型如何变化,它一定是new String()的父类(或它自己)啊!
泛型递归
啥,泛型还能递归?当然能。而且这也是一种好玩的泛型使用:
1
2
3
4
5
6
7
8
|
class
Wrapper
@Override
public
int
compareTo(Wrapper
return
0
;
}
}
|
好玩吧?泛型也能递归。这个例子指的是,一个对象E由包装器Wrapper所包装,但是,E也必须是一个包装器,这正是包装器的递归;同时,包装器也实现了一个比较接口,使得两个包装器可以互相比较大小。
别晕!泛型只不过是一个普普通通的语言特性,但是也挺有趣的。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》