Java 泛型(generics)是 JDK 5 引入的一个特性,它提供了编译时类型安全检测机制,允许开发者在编译期就发现非法的类型操作。泛型的本质就是参数化类型,也就是说,所操作的数据类型被指定为一个参数。若想了解更多 Java 泛型的底层细节与最佳实践,可以在云栈社区的技术专区深入交流。
泛型带来的好处
在没有泛型的时代,通常用 Object 引用来实现参数的“任意化”。但“任意化”的代价是必须进行显式的强制类型转换,而这要求开发者能提前预知实际参数的类型。一旦强制转换错误,编译器可能不会报错,到运行时才抛异常,这本身就是一个安全隐患。
泛型的优势就在于:编译时就能检查类型安全,并且所有的强制转换都是自动、隐式的。
public class GlmapperGeneric<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
public static void main(String[] args) {
// do nothing
}
/**
* 不指定类型
*/
public void noSpecifyType(){
GlmapperGeneric glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 需要强制类型转换
String test = (String) glmapperGeneric.get();
System.out.println(test);
}
/**
* 指定类型
*/
public void specifyType(){
GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 不需要强制类型转换
String test = glmapperGeneric.get();
System.out.println(test);
}
}
上面的 specifyType 方法省略了强制转换,可以在编译期检查类型安全,这种机制可应用于类、方法、接口。
泛型中通配符
在定义泛型类、泛型方法、泛型接口时,经常会见到形如 T、E、K、V 等不同的通配符。它们到底有什么含义呢?
常用的 T、E、K、V、?
本质上,这些只是通配符的约定俗成,并没有本质区别。比如上面代码中的 T,可以换成 A‑Z 之间任何一个字母,程序都能正常运行,只是换成别的字母可读性会变弱。通常的约定如下:
? 表示不确定的 Java 类型
T (type) 表示具体的一个 Java 类型
K V (key value) 分别代表 Java 键值中的 Key 与 Value
E (element) 代表 Element(元素)
? 无界通配符
先从一个小例子看起。
我有一个父类 Animal 和几个子类,如狗、猫等。现在需要一个动物的列表,我的第一想法是这样:
List<Animal> listAnimals
不过,更推荐的做法是:
List<? extends Animal> listAnimals
为什么要用通配符而不是简单的泛型?其实,单纯声明局部变量时通配符没什么意义,但当你在方法参数中使用时,它就非常重要了。
static int countLegs (List<? extends Animal > animals ) {
int retVal = 0;
for ( Animal animal : animals ) {
retVal += animal.countLegs();
}
return retVal;
}
static int countLegs1 (List< Animal > animals ) {
int retVal = 0;
for ( Animal animal : animals ) {
retVal += animal.countLegs();
}
return retVal;
}
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
// 不会报错
countLegs( dogs );
// 报错
countLegs1(dogs);
}
调用 countLegs1 时会直接飘红,报错如下:

因此,对于不确定或者不关心实际要操作的类型,可以使用无界通配符(尖括号里一个问号,即 <?>),表示可以持有任何类型。像 countLegs 方法,限定了上界 extends Animal,但不关心具体是什么子类,所有的 Animal 子类都能正常传入。而 countLegs1 就没法做到这一点。
上界通配符 <? extends E>
上界:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是该类型的子类。
使用 <? extends E> 在类型参数里有两大好处:
- 如果传入的类型不是
E 或 E 的子类,编译直接失败
- 泛型内部可以直接使用
E 的方法,不用再强转
private <K extends A, E extends B> E test (K arg1, E arg2) {
E result = arg2;
arg2.compareTo(arg1);
//.....
return result;
}
类型参数列表中如果有多个上限,用逗号分开。
下界通配符 <? super E>
下界:用 super 声明,表示参数化的类型可能是所指定的类型,或者是该类型的父类型,最远可以至 Object。
在类型参数中使用 super,意味着这个泛型参数必须是 E 或 E 的父类。
private <T> void test (List<? super T> dst, List<T> src) {
for (T t : src) {
dst.add(t);
}
}
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = new ArrayList<>();
new Test3().test(animals,dogs);
}
// Dog 是 Animal 的子类
class Dog extends Animal {
}
dst 的类型 “大于等于” src 的类型(这里的 “大于等于” 指 dst 代表的类型范围更广),因此能装下 src 的容器自然也能装下 dst。
?和 T 的区别

? 和 T 都表示不确定的类型,但区别在于我们可以对 T 进行操作,而对 ? 不行。比如:
// 可以
T t = operate();
// 不可以
?car = operate();
简单总结:
T 是一个 确定的 类型,通常用在泛型类和泛型方法的定义中;? 是一个 不确定 的类型,通常用于泛型方法的调用和形参,不能用来定义类和泛型方法。
区别1:通过 T 来确保泛型参数的一致性
// 通过 T 来确保泛型参数的一致性
public <T extends Number> void test(List<T> dest, List<T> src)
// 通配符是不确定的,所以此方法不能保证两个 List 具有相同的元素类型
public void test(List<? extends Number> dest, List<? extends Number> src)
下面的代码,因为 String 不是 Number 的子类,所以直接飘红报错。

而如果两个 List 的元素类型不一致的情况,如下:
GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric<>();
List<String> dest = new ArrayList<>();
List<Number> src = new ArrayList<>();
glmapperGeneric.testNon(dest,src);
上面这段代码在编译器不会报错,但进入 testNon 方法内部操作时(比如赋值),对于 dest 和 src 就还是需要手动进行类型转换。
区别2:类型参数可以多重限定而通配符不行

使用 & 符号可以设定多重边界(Multi Bounds),指定泛型类型 T 必须同时是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就拥有了所有限定的方法和属性。通配符因为不是确定的类型,所以无法进行多重限定。
区别3:通配符可以使用超类限定而类型参数不行
类型参数 T 只具有 一种 类型限定方式:
T extends A
但通配符 ? 可以进行 两种限定:
? extends A
? super A
Class<T> 和 Class<?> 的区别
前面介绍了 ? 和 T 的区别,那么 Class<T> 和 Class<?> 又有什么不同呢?
反射场景中的使用尤为常见,这里用一段反射代码来说明。
// 通过反射的方式生成 multiLimit
// 对象,这里比较明显的是,我们需要使用强制类型转换
MultiLimit multiLimit = (MultiLimit)
Class.forName("com.glmapper.bridge.boot.generic.MultiLimit").newInstance();
对于上述代码,在运行期如果反射出来的类型不是 MultiLimit 类,就会抛出 java.lang.ClassCastException 异常。
更好的写法是让编译器提前检查类型问题:

Class<T> 在实例化时,T 必须替换成具体的类。而 Class<?> 是一个通配泛型,? 可以代表任意类型,所以主要用于声明时的限制。比如:
// 可以
public Class<?> clazz;
// 不可以,因为 T 需要指定类型
public Class<T> clazzT;
那如果也想使用 public Class<T> clazzT; 这样的声明,就必须让所在的类也指定 T:
public class Test3<T> {
public Class<?> clazz;
// 不会报错
public Class<T> clazzT;
}
当你不确定要声明什么类型的 Class 时,可以定义一个 Class<?>。

小结
本文零碎整理了 Java 泛型中的一些要点,难免有不全之处,仅供参考。若发现不当之处,欢迎指正。
如果想要了解更多 Java 技术干货,欢迎访问 云栈社区 与众多开发者一起交流成长。