【从零单排】关于泛型Generic的一些思考
相信很多同学都多多少少听过Generic-泛型这个概念。下面就结合实际的例子对泛型进行讲解和剖析。
为什么要用泛型
泛,有多的意思。型,即为类型。结合起来,就是可以用泛型来指代“多种类型”,从而可以复用函数或类。
举例:写一个echo函数用来输出List种某两个位置的值。
当List里面装的是String,就是这样:
public static void echo_string(List<String> a, int i, int j) {
String tmp_i = a.get(i);
String tmp_j = a.get(j);
System.out.println(tmp_i);
System.out.println(tmp_j);
}
当List里面装的是Integer,就是这样:
public static void echo_integer(List<Integer> a, int i, int j) {
int tmp_i = a.get(i);
int tmp_j = a.get(j);
System.out.println(tmp_i);
System.out.println(tmp_j);
}
每次根据List里面值的不同类型,写一个对应的函数,不免有点重复劳动。能不能用一个函数实现呢?这就需要用到泛型。如下:
public static <T> void echo_generic(List<T> a, int i, int j) {
T tmp_i = a.get(i);
T tmp_j = a.get(j);
System.out.println(tmp_i);
System.out.println(tmp_j);
}
什么地方用泛型
主要有两个地方:
- 创建 - 函数
- 创建 - 类(Class)
函数的例子上面已经举过了,不再赘述。下面就讲讲类。
class SomeList<T>{
private T param;
SomeList(T param) {
this.param = param;
System.out.println("constructor for SomeList");
}
public T getParam() {
return param;
}
public void setParam(T param) {
this.param = param;
}
}
SomeList<String> sl1 = new SomeList<>("123");
System.out.println(sl1.getParam());
SomeList<Integer> sl2 = new SomeList<>(123);
System.out.println(sl2.getParam());
在这个例子中,我们定义了一个SomeList类,它可以是String类型的,也可以是Integer类型的,取决于初始化时的传参类型。
泛型的符号
在实际使用过程中,我们常常看到<T>
,或者是<K,V>
这样的符号,被IDE高亮显示为不同的颜色。如下:
public static <T> List<T> swap(List<T> a, int i, int j) {
T tmp_i = a.get(i);
T tmp_j = a.get(j);
a.set(i, tmp_j);
a.set(j, tmp_i);
return a;
}
public static <K,V> V getValue(K key, Map map) {
return (V) map.get(key);
}
我不禁要问:<T>
,<K,V>
是Java的保留字符吗?经过一些尝试之后,得出结论:不是的。这里用(几乎)任何的字母都可以。方括号之内可以是一个或多个字符,用来告诉编译器这个是我自定义的type。
比如,上述的代码,可以把T,K,V替换成其它的字母,改写如下:
public static <M> List<M> swap2(List<M> a, int i, int j) {
M tmp_i = a.get(i);
M tmp_j = a.get(j);
a.set(i, tmp_j);
a.set(j, tmp_i);
return a;
}
public static <X,Y> Y getValue2(X key, Map map) {
return (Y) map.get(key);
}
甚至,我们可以定义大于2个的type,比如<X,Y,Z>
:
public static <X,Y,Z> Z getValue3(X key, Y key2, Map map1, Map map2) {
return (Z) (map1.get(key).toString() + map2.get(key2).toString());
}
通配符 Wildcards
关于Wildcards怎么用我思考了很久,结合实际经验,我觉得单独使用Wildcards的情景很少,一般都是使用Bounded Wildcards。
Wildcards
单独使用Wildcards的情景,用Generic也可以实现。如下:
void printCollection1(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
<T> void printCollection2(Collection<T> c) {
for (Object e : c) {
System.out.println(e);
}
}
唯一的区别就是,用Wildcards的话,理论上传入参数Collection可以是多种不同的类型。但是其实Collection在赋值的时候,是没办法装不同类型的值的。
Collection<?> c = new ArrayList<>();
c.add(1);
// 编译时会报错 add(capture<?>) cannot be applied to (int)
所以个人感觉一般编程不太需要单独用到Wildcards,大多数情况可以用Generic覆盖。
Bounded Wildcards
而Bounded Wildcards,是有确实的用途的,可以给传入参数限制类型。
这里分了两种extends
和super
:
- (上限) The extends Wildcard Boundary:
List<? extends A>
means a List of objects that are instances of the class A, or subclasses of A - (下限) The super Wildcard Boundary:
List<? super A>
means that the list is typed to either the A class, or a superclass of A.
分析extends
,举例来说,之前我们定义了一个echo_generic方法,这个方法里面的传入参数可以是任意类型的List。现在,我们想要写一个类似的函数,但是List的类型只能是Number(Integer,Double等等都算)。如下:
public static void echo_number(List<? extends Number> a, int i, int j) {
Object tmp_i = a.get(i);
Object tmp_j = a.get(j);
System.out.println(tmp_i);
System.out.println(tmp_j);
}
具体使用的时候,我们可以发现,确实只能传入Number或其subclass类型的List,否则编译时就会报错。
// integer
List<Integer> list_integer = new ArrayList<>();
list_integer.add(1);
list_integer.add(2);
echo_number(list_integer, 0, 1);
// double
List<Double> list_double = new ArrayList<>();
list_double.add(1.1);
list_double.add(2.2);
echo_number(list_integer, 0, 1);
// string - compile error
List<String> list_string = new ArrayList<>();
list_string.add("1");
list_string.add("2");
echo_number(list_string, 0, 1);
编译时的泛型
问:一个ArrayList<String> collection1
和一个ArrayList<Integer> collection2
,它们的class是一样的吗?如下:
ArrayList<String> collection1 = new ArrayList<String>();
collection1.add("123");
ArrayList<Integer> collection2 = new ArrayList<Integer>();
collection2.add(456);
System.out.println(collection1.getClass() == collection2.getClass());
答案是:是一样的。原因很简单,我们只创建了一个类class ArrayList<T>
,collection1
和collection2
的class都是这个。泛型的作用在于,在编译的时候,会限定集合中的值的类型。collection1
中的值只能是String类型的,collection2
中的值只能是Integer类型的。
由此,想到一个黑科技,在runtime时,可以往一个ArrayList<String>
里面塞Integer
吗(即跳过编译过程时的泛型检查)?
答案是:可以的。
try {
collection1.getClass().getMethod("add", Object.class).invoke(collection1, 789);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(String.valueOf(collection1.get(1)));
// 789
但是,这样做,首先是污染了collection1这个object;其次是往往需要在取出值的时候特别注意,正确转换数据类型,否则容易报错。所以并不是一个推荐的常规操作。