Java中的泛型
泛型(Generics), 大家应该都见过, 一堆奇奇怪怪的符号, 诸如T、K、V之类, 还有问号、通配符等等. 有时出现在类中, 有时出现在方法中, 那么我们要如何理解和正确的使用泛型这个概念?

为什么引入泛型?
要搞清楚这个问题, 我们首先要知道在没有泛型之前, 我们是如何怎么解决问题的?
比如我现在有一个需求, 我们需要创建一个类然后打印Integer变量
public class IntegerPrinter {
Integer content;
public IntegerPrinter(Integer content) {
this.content = content;
}
public void printContent() {
System.out.println("content: " + content);
}
}
以上是一个最简单的例子, 但后边可能会有其他需求, 比如需要打印一个String变量, 那么我们便不能继续使用这个类, 需要创建一个新的StringPrinter类去处理该需求
public class StringPrinter {
String content;
public StringPrinter(String content) {
this.content = content;
}
public void printContent() {
System.out.println("content: " + content);
}
}
这样就带来了一个问题, 比如现在需要打印一个Double变量, 那么就需要再创建一个DoublePrinter类, 以此类推, 代码便会复杂不堪,
这是我们必须要避免的, 也正因为如此, 我们引入了泛型这样的概念, 通过泛型, 我们便可以在使用同一个类的情况下处理不同类型;
简单实践
如何创建一个泛型类? 很简单, 只需要在类的主体大括号与类名之间加上泛型参数即可,
public class Printer<T> {
T content;
public Printer(T content) {
this.content = content;
}
public void printContent() {
System.out.println("content: " + content);
}
}
相关信息
<T>
便是你的泛型占位符, 理论上可以使用任意字符, 比如<K>、<Anything>
等等;
当然, 泛型类中也可以传入多个占位符, 比如public class Printer<T, K, V>
等等;
调用方式很简单, 只需要在类名后边加上要使用的类型即可, 比如
// 整形打印
Printer<Integer> integerPrinter = new Printer<>(123);
integerPrinter.printContent();
// 字符串打印
Printer<String> stringPrinter = new Printer<>("Hello World");
stringPrinter.printContent();
到这里, 我们如果需要再去打印一个Double变量, 我们只需要在类名与泛型参数之间加上Double
即可, 而不需要再去创建一个DoublePrinter类;
重要
泛型的类型参数只能是引用类型, 不能用于基本类型, 比如Printer<int>、Printer<char>
是错误的;
约束
很多时候, 我们需要限制泛型类型, 比如我们希望泛型类型只能是Number的子类, 那么我们可以在泛型参数后边加上extends
关键字
public class Printer<T extends Number> {
...
}
这时, 我们的泛型类将会便为一个有界限的泛型, 他只能接受Number及其子类, 比如Integer、Double、Float等等, 而不能是String、Character、Boolean等等;
与class约束的方式类似, 我们也可以限制泛型类型只能是interface的实现类, 同样是使用extends
关键字, 比如
public class Printer<T extends Serializable> {
...
}
那么如果我们希望泛型类不仅要是Number的子类, 还要实现一个接口, 那么我们可以在泛型参数后边加上&
关键字
public class Printer<T extends Number & Serializable> {
...
}
重要
在使用extends
时, class必须要放在interface之前, 比如extends interface & class
是错误的;
由于java单继承、多实现的特性, 所以你可以同时使用多个interface进行约束, 格式为extends interface1 & interface2 & ...
, 比如:
public class Printer<T extends Number & Comparable & Constable> {
...
}
为什么要进行约束?
其实在日常中我们一直在有意无意的使用着泛型, 最常见的应属集合框架(Collection), 比如List、Set、Map等等;
比如下边这段代码
List<Object> list = new ArrayList<>();
list.add("hello world");
list.add(123);
String item = (String) list.get(0);
System.out.println(item);
item = (String) list.get(1);
System.out.println(item);
编译时似乎并没有什么异常, 但实际上在运行时, 会抛出ClassCastException
, 无法将Integer转换为String;
这里便会存在一个类型安全的问题, 这是因为泛型的工作阶段是在编译时进行类型检查的, 而不是在运行时;
泛型方法
泛型也经常用在方法上, 泛型方法与泛型类基本类似, 我们只需要在返回类型之前加上泛型参数即可
private static <T> void print(T content) {
System.out.println("content: " + content);
}
如果想要约束泛型类型, 或传入多个泛型参数, 与泛型类的处理方式基本类似
private static <T> void print(T content) {
...
}
private static <T, K> void print(T content, K content2){
...
}
private static <T extends Number, K extends Serializable> void print(T content, K content2){
...
}
相关信息
泛型方法并非一定要在泛型类的内部, 它是一个完全独立的方法;
通配符
上边的所有方式中, 都是仅能接受自身及子类, 当我们希望传入自身以及父类时, 可以使用通配符, 比如你想使用List<Number>
来处理Integer、Double、Float等
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(123);
list.add(456);
print(list); // 编译错误
}
private static void print(List<Number> content) {
...
}
你会发现编译不通过, 因为前边提到, List自身也是一个泛型类, 虽然直观来看Integer是Number的子类, 但List<Integer>
和 List<Number>
仍然是两个不同的类型, 因此编译器会报错;
当然, 你仍然可以按照之前的约束方式来解决这个问题
private static <T extends Number> void print(List<T> content) {
...
}
但现在有了更加灵活的解决方法, 那就是使用<?>
通配符, 他可以接受任意类型, 比如
private static void print(List<?> content) {
...
}
他与List<Object>
类似, 像这样直接使用时, 仍然可能发生类型安全的问题, 所以当其作为入参时, 应当进行一定的约束
private static void print(List<? extends Number> content) {
...
}
这是就延伸除了一个新的概念: "上限通配符", 表示仅接受自身以及子类, 如List<Integer>、List<Double>、List<Number>
等;
既然有"上限", 当然也有"下限", 比如
private static void print(List<? super Integer> content) {
...
}
使用super
关键字进行约束时, 便是"下限通配符", 表示仅接受自身以及父类, 如List<Integer>、List<Number>、List<Object>
等;
PECS原则
那么什么时候要使用下限通配符, 什么时候使用上限通配符呢? 直接上答案:
如果你是想遍历Collection, 并对每一项元素操作时, 此时这个集合是生产者(生产元素), 应该使用 Collection<? extends Thing>
;
如果你是想添加元素到Collection中去, 那么此时集合是消费者(消费元素), 应该使用Collection<? super Thing>
;
重要
这便是PECS原则, 即Producer Extends, Consumer Super
;