一. 为什么需要泛型
先来说说一个简单的实例。
class Store {
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
public class GenericTest {
public static void main(String[] args) {
Store store = new Store();
store.setData("zhang");
// 这里必须强转
String data = (String) store.getData();
System.out.println(data);
}
}
这个例子很简单,有一个仓库Store,里面储存了一个数据data,我们在主函数中向仓库中存入数据,然后又取出数据。为了让仓库Store可以存储各种各样的数据,所以数据data的类型是Object。
但是这里有个问题,如果我们不小心存了一个int数据类型,而取数据时又把它强转成String类型,那么就会抛出ClassCastException异常,程序终止。
这种错误是非常严重的,它会导致程序的崩溃,那么我们有没有好的方法来阻止这类错误的产生呢?这里产生问题的原因是我们添加了错误类型的数据,那么我们就必须提醒开发者,这里是错误操作。
要解决这类问题,就需要使用到泛型。
二. 泛型简单使用
class Store<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
public class GenericTest {
public static void main(String[] args) {
Store<String> store = new Store();
// 不能设置int类型,只能设置String以及String的子类
store.setData(1);
// 不需要强制转换
String data = store.getData();
System.out.println(data);
}
}
将上面例子改成泛型实现,会发现有三点变化:
- Store<String>: 参数化类型的Store,它的实际类型参数是String,关于这个含义在下一章介绍。
- 当向Store存入int类型,会直接报错,告诉开发者不能存入int类型,只能存入String以及String的子类。
- 取出数据时,不需要强制转换。
这里我们就看到了泛型T最大的作用:通过编译器检查,来约束T的接受类型。
例如这里对于Store<String>这个参数化类型,它的setData方法参数T的接受类型只能是String以及String的子类。因此它的getData方法返回的T类型就肯定是String或者String的子类,所以可以直接用String类型接受,不需要强制转换。
三. 泛型参数意义
在上面的例子涉及到三个重要术语:
- 类型参数(type parameter): 就是 Store<T>中的T。类型参数可以是一个或者多个。
- 泛型(generic): 声明中具有一个或者多个类型参数的类或者接口都称为泛型。例如Store<T>
- 参数化类型(parameterized type): 就是用实际类型参数替代泛型声明中的类型参数。即这里的Store<String>。
除了这些,泛型还有原生态(raw type)类型,无限制通配符类型(unbounded wildcard type),和有限制通配符类型(bounded wildcard type)等等,将在后面介绍。
四. 泛型子类型
如果我们想创建一个新方法,来打印Store中的存储的数据data,例如这样:
public class GenericTest {
// 打印仓库Store中存储的数据data
public static void print(Store<String> store) {
String str = store.getData();
System.out.println(str);
}
public static void main(String[] args) {
Store<String> sStore = new Store();
sStore.setData("zhang");
print(sStore);
}
}
但是现在print方法只能接受Store<String>这个参数化类型,我们想让方法更加通用,可以打印任何数据,那么很容易想到这样进行改动:
public class GenericTest {
// 打印仓库Store中存储的数据data
public static void print(Store<Object> store) {
Object obj = store.getData();
System.out.println(obj.toString());
}
public static void main(String[] args) {
Store<String> sStore = new Store();
sStore.setData("zhang");
// 这里报错。就是不能将Store<String>实例赋值给Store<Object>对象实例
print(sStore);
}
}
这样改动后,发现程序居然报错了,Store<String>实例赋值给Store<Object>对象实例。
这个就很奇怪了,按照多态理论,子类对象可以赋值给父类对象。String是Object的子类,但是Store<String>对象居然不能赋值给Store<Object>对象。也叫做泛型是不协变的(covariant)。
4.1 泛型是不协变的
在java中数组是协变的,即如果Sub是Super的子类型,那么数组类型Sub[]也是Super[]的子类型。但是对于泛型来说,参数化类型Store<Sub>却不是参数化类型Store<Super>的子类型。
那么泛型为什么不支持协变呢?主要是因为泛型的作用约束:
对于一个泛型Store<T>,如果我们创建了一个参数化类型Store<String>的对象sStore,当我们设置T值的时候(即setData(T data)方法),编译器会检查,保证设置的T值只能是String以及String子类。
但是如果我们泛型支持协变的话,就会破坏这个编译器检查,但是泛型失去作用。
例如,下面这个例子:
public static void main(String[] args) {
Store<String> sStore = new Store();
// 如果泛型支持协变,这里赋值允许
Store<Object> store = sStore;
// 向store存储一个整数类型是可以的,因为Integer是Object的子类
store.setData(new Integer(10));
// 从sStore获取数据,因为是泛型不需要强制转换
String s = sStore.getData();
}
如果泛型支持协变,那么我们就可以通过store向参数化类型Store<String>实例sStore中,设置整数类型变量,因为store表明的参数化类型是Store<Object>,所以可以设置整数类型变量(Integer是Object子类).
这样就躲过了泛型的编译器检查,导致sStore储存了Integer类型,泛型的约束就不存在了。
4.2 原生态(raw type)类型
我们知道泛型是不协变的,那么想实现通用方法,即可以实现接受不同参数化类型参数,该如何实现呢?
public class GenericTest {
// 打印仓库Store中存储的数据data
public static void print(Store store) {
Object obj = store.getData();
System.out.println(obj.toString());
}
public static void main(String[] args) {
Store<String> sStore = new Store();
sStore.setData("zhang");
print(sStore);
}
}
这里代码不会报错,我们使用原生态类型Store接受参数化类型Store<String>。
所谓原生态类型,就是移除了所有泛型信息,就相当于没有使用泛型一样,那么也就没有编译器检查,已经泛型约束了。就如同最开始使用Object类型data的那个Store。
在代码中,我们尽量不要使用原生态类型,因为它会破坏泛型约束,导致安全性问题:
public static void main(String[] args) {
Store<String> sStore = new Store();
// 使用原生态类型Store来接收
Store store = sStore;
// 向store存储一个整数类型是可以的。默认可以接收任何类型
store.setData(new Integer(10));
// 从sStore获取数据,因为是泛型不需要强制转换。
// 运行时,这里直接抛出ClassCastException异常
String s = sStore.getData();
}
使用原生态类型导致程序运行时,抛出了ClassCastException异常,因为我们向store储存了整数数据。
4.3 无限制通配符类型(unbounded wildcard type)
原生态类型是不安全的,可能会抛出异常。java中提供了一种安全的替代方法,就是无限制通配符类型。如果我们不关心实际类型参数,那么我们可以使用问号?来替代,即Store<?>。
无限制通配符类型可以接受任何参数化类型,而且它保证了泛型的安全。
public static void main(String[] args) {
Store<String> sStore = new Store();
// 使用无限制通配符类型Store<?>来接收
Store<?> store = sStore;
// 这里报错。不能向Store<?>类型存储任何值,除非是null
store.setData(new Integer(10));
// 任何参数化类型获取的值,都是Object类型或者其子类。
Object obj = store.getData();
// 从sStore获取数据,因为是泛型不需要强制转换。
// 因为不能通过store设置值,所以sStore还是只能由它自身设置,
// 因为有泛型约束,不会有强转问题。
String s = sStore.getData();
}
与原生态类型相比较,不允许向无限制通配符类型设置任何值。
- 无限制通配符类型可以接受任何参数化类型,如果可以给它设置值,例如这里store接受的是Store<String>的实例sStore,向store中设置了整型数据,那么从sStore获取数据时,就会有强转异常。所以就不允许设置值。
- 从无限制通配符类型获取值是可以的,但是获取值的类型只能是Object类型。
4.4 有限制通配符类型(bounded wildcard type)
无限制通配符类型虽然解决了泛型安全问题,但是有两个缺点:
- 不能设置值。
- 只能获取Object类型的值。
针对这个两个缺点,我们使用有限制通配符类型来解决。
4.4.1 有限制通配符super
为什么不能设置值,是因为无限制通配符类型可以接受任何参数化类型,所以我们没有办法设置任何类型,因为对于任何一个具体参数化类型,都有可能是错误的。
例如我们在setData设置的是Integer类型,但是store接收的参数化类型Store<String>,那么就产生了错误。
怎么解决这个问题呢?我们可以让store接收一定限制的参数化类型,而不是任何参数化类型。
可以让store接收实际类型参数是String以及String父类的参数化类型,即Store<String>、Store<CharSequence>、Store<Object>等等。
对于这些参数化类型,它们都可以设置String类型的数据,这是因为多态的原因。
public static void main(String[] args) {
Store<String> sStore = new Store();
// 使用有限制通配符 Store<? super String>来接收
Store<? super String> store = sStore;
// 可以向Store<? super String>中存储String以及String子类型的值
store.setData("zhang");
// 获取Object类型是可以的。因为Object本来就可以代表任何类型
Object obj = store.getData();
// 这里是错误的,获取String类型是不可以的。
// 因为store可能是Store<CharSequence>参数化类型,
// 获取的值就不一定是String类型。
String str = store.getData();
// 这里是正确的。
String s = sStore.getData();
}
与无限制通配符类型相比较:
- 对于Store<? super Sub>来说,它可以接收实际类型参数是Sub以及Sub父类的参数化类型,它可以设置Sub以及Sub子类的数据。
- 也是只能获取Object类型数据。这里可能会有疑惑,我们不是设置Sub以及Sub子类的数据,那么获取的数据应该也是Sub类型啊。那是因为我们不只是通过有限制通配符类型来设置数据啊。
public static void main(String[] args) {
Store<CharSequence> cStore = new Store();
// 使用有限制通配符 Store<? super String>来接收
Store<? super String> store = cStore;
// 可以向Store<? super String>中存储String以及String子类型的值
store.setData("zhang");
// 通过cStore设置StringBuilder类型
cStore.setData(new StringBuilder("sb"));
// 这里是错误的。store中存储的是StringBuilder类型,强转错误。
String str = store.getData();
}
在这个方法中,我们通过参数化类型cStore来设置值,导致类型错误。所以从super有限制通配符中获取数据类型也只能是Object类型,因此它只能解决设置值的问题。
4.4.2 有限制通配符extends
要想获取指定类型的数据。我们也要让store接收一定限制的参数化类型,而不是任何参数化类型。
可以让store接收实际类型参数是String以及String子类的参数化类型。那么我们就可以从中获取String类型的数据。
public static void main(String[] args) {
Store<String> sStore = new Store();
// 使用有限制通配符 Store<? extends String>来接收
Store<? extends String> store = sStore;
// 不可以向Store<? extends String>类型中储存任何值。除非是null
store.setData("zhang");
// 因为store接受的参数化类型的实际参数类型是String以及String子类。
// 所以从中获取的数据一定可以用String类型接受
String str = store.getData();
// 这里是正确的。
String s = sStore.getData();
}
与无限制通配符类型相比较:
- 对于Store<? extends Sub>来说, 它可以接收实际类型参数是Sub以及Sub子类的参数化类型,因此它获取的数据一定可以用Sub类型接受。
- 不能向Store<? super Sub>中设置任何类型的数据。
public static void main(String[] args) {
Store<Integer> iStore = new Store<>();
// 将iStore赋值给store变量
Store<? extends Number> store = iStore;
// 假如这里可以设置Number类型的数据。
store.setData(new Double(1.0));
// 获取Number类型的数据是没问题的
Number number = store.getData();
// 这里就有问题了,因为我们存入的是Number类型,
// 而获取的是Integer,就有可能出现错误
Integer i = iStore.getData();
}
如果可以设置数据,就会破坏泛型的约束,产生安全问题。
4.5 小结
无限制通配符类型与有限制通配符类型都是只能作为接收类型,而不能作为创建类型。只能创建含有实际类型参数的参数化类型泛型。
// 错误,不能创建无限制通配符类型的泛型
Store<?> store1 = new Store<?>();
// 错误,不能创建有限制通配符类型的泛型
Store<? extends Number> store2 = new Store<? extends Number>();
// 错误,不能创建有限制通配符类型的泛型
Store<? super Number> store3 = new Store<? super Number>();
// 正确,创建参数化类型
Store<Number> store4 = new Store<Number>();
// 正确,创建参数化类型,省略实际类型参数
Store<Number> store5 = new Store<>();
// 正确,创建参数化类型,省略实际类型参数
Store<Number> store6 = new Store();
- 无限制通配符类型Store<?>:可以接收任何参数化类型。为了保证泛型约束,所以不能向无限制通配符类型设置任何数据,从无限制通配符类型中只能获取到Object类型的数据。
- super有限制通配符类型Store<? super Sub>:只能接收实际类型参数是Sub以及Sub父类的参数化类型。所以可以向super有限制通配符类型中设置Sub以及Sub子类类型的数据。但是也只能从中获取Object类型数据。
- extends有限制通配符类型Store<? extends Sub>:只能接收实际类型参数是Sub以及Sub子类的参数化类型。所以从中获取数据一定可以被Sub类型接收。但是也不能向super有限制通配符类型设置任何数据。
总结就是:
Store<?>不能设置数据,但是能获取Object类型的数据。
Store<? super Sub>能设置Sub类型的数据,但是只能获取Object类型的数据。
Store<? extends Sub>不能设置数据,但是能获取Sub类型的数据。
五. 泛型方法
我们知道java中方法通常由四部分组成:1. 修饰符:public、protected、private或者没有修饰符。2. 方法返回值。3. 方法名。4. 方法参数。如果方法是静态方法,还要加上static关键字。
那么什么是泛型方法?就是在修饰符之后和方法返回值之前添加泛型声明。
public <T> Store<T> genericFun(Store<T> store) { }
泛型方法的作用是什么呢?假如我们有一个方法来复制Store变量。首先我们尝试不用泛型方法:
public static void main(String[] args) {
Store<Integer> store = new Store<>();
store.setData(100);
Store<Integer> copy = copy(store);
System.out.println(copy.getData());
}
private static Store<Integer> copy(Store<Integer> store) {
Store<Integer> result = new Store<>();
Integer t = store.getData();
result.setData(t);
return store;
}
但是如果我们想复制Store<String> 变量,就会发现copy方法不能使用了。要使copy方法可以针对任何参数化类型,那么就必须使用泛型方法。
public static void main(String[] args) {
Store<Integer> store = new Store<>();
store.setData(100);
Store<Integer> copy = copy(store);
System.out.println(copy.getData());
}
private static <T> Store<T> copy(Store<T> store) {
Store<T> result = new Store<>();
T t = store.getData();
result.setData(t);
return store;
}
现在我们有了新的需求,可以比较Store中储存数据的大小。
public static void main(String[] args) {
Store<Integer> store1 = new Store<>();
store1.setData(100);
Store<Integer> store2 = new Store<>();
store2.setData(200);
Store<Integer> store = max(store1, store2);
System.out.println(store.getData());
}
private static <T extends Comparable<T>> Store<T> max(
Store<T> store1, Store<T> store2) {
T t1 = store1.getData();
T t2 = store2.getData();
T t = t1.compareTo(t2) > 0 ? t1 : t2;
Store<T> result = new Store<>();
result.setData(t);
return result;
}
这里的类型参数变得不一样了,它不再是简简单单地一个字母T,而是T extends Comparable<T>。在泛型的声明中有下列三种格式:
- T:类型参数
- T extends Number: 有限制类型参数
- T extends Comparable<T>: 递归类型限制
注:这个与有限制通配符类型是两个概念,这个是用在泛型声明中。还有它不支持super关键字。
如果我们声明泛型的类型参数是Comparable的子类型,那么就可以将它当成Comparable变量使用,就可以调用Comparable中的方法。
但是这种实现方式还是有点缺陷,参数和返回值都必须是同一个参数化类型,就如这里只能是Store<Integer>参数化类型, 那我们想比较Store<Integer>与Store<Double>大小应该怎么改动呢?
我们想到了上一节说的有限制通配符类型,对Store<T>来说,我们想从Store<T>中获取值,那么就使用extends有限制通配符类型Store<? extends T>。
public static void main(String[] args) {
Store<Integer> store1 = new Store<>();
store1.setData(100);
Store<Double> store2 = new Store<>();
store2.setData(200.0);
// 注意:这里编译不通过,报错。
/**
* Integer和Double有共同的父类型Number,
* 使用Store<? extends T>来接收这两个参数化类型是没问题的。
* 那么这里报错的原因是什么呢?那是因为我们对T有限制,T必须是Comparable的子类型。
* Integer和Double都实现了Comparable接口,是Comparable的子类型,
* 但是它们的父类型Number没有实现Comparable接口,所以这里推导T是Number类型,
* 但是又不满足泛型声明时T extends Comparable<T>条件,
* 所以这下面句代码报错。除非它们两个父类型也是Comparable接口的子类型,才能通过。
*/
Store<Number> store = max(store1, store2);
System.out.println(store.getData());
}
private static <T extends Comparable<T>> Store<T> max(
Store<? extends T> store1, Store<? extends T> store2) {
// 从Store<? extends T>中获取值,没问题
T t1 = store1.getData();
T t2 = store2.getData();
T t = t1.compareTo(t2) > 0 ? t1 : t2;
Store<T> result = new Store<>();
result.setData(t);
return result;
}
很遗憾,这里直接报错了。Integer和Double有共同的父类型Number,那么使用Store<? extends T>来接收这两个参数化类型是没问题的,T就是Number类型,但是Number类型不是Comparable接口子类型,所以它没有compareTo的方法,运行max方法就会产生错误。
所以这里max(store1, store2)代码直接报错,除非它们两个父类型也是Comparable接口的子类型,才能通过。
六. 总结
泛型分为两个部分:
6.1 泛型声明
- <T>: 类型参数
- <T extends Number>: 有限制类型参数
- <T extends Comparable<T>>: 递归类型限制
泛型声明一般用在类、接口或者泛型方法上。
6.2 泛型实例
- 参数化类型:如Store<String>,其中String是它的实际类型参数。记住创建泛型实例只能创建参数化类型实例,也就是说创建泛型实例,必须要确定实际类型参数,否则是无法创建的。
- 原生态类型Store:它可以接收任何参数化类型,并且它能设置任何类型数据,还可以获取Object类型的数据。所以它不能保证泛型约束,运行时可能会抛出异常。
- 无限制通配符类型Store<?>:它可以接收任何参数化类型,但是不能设置数据,只能能获取Object类型的数据。与原生态类型相比较,它保证了泛型约束。
- super有限制通配符类型Store<? super Sub>:只能接收实际类型参数是Sub以及Sub父类的参数化类型。能设置Sub类型的数据,但是只能获取Object类型的数据。
- extends有限制通配符类型Store<? extends Sub>:能接收实际类型参数是Sub以及Sub子类的参数化类型。不能设置数据,但是能获取Sub类型的数据。