什么是泛型
泛型可以理解为一个模板,但是与接口抽象类不一样,它规范的是类型,而不是内部的行为。
以List为例子,它是一个接口,我们通常使用的是它的实现类–ArrayList,使用它们我们可以代替声明一个定长的数组,因为ArrayList是变长的,当然它们的区别还不仅于此,List还使用了泛型,所以在使用的时候我们要声明List保存的数据的类型,如下:
int[] ints = new int[32];
List<Integer> intlist = new ArrayList<Integer>();
这样我们就声明了一个int类型的数组和List,实际上List的内部还是封装了一个数组,int是基本数据类型,如果我们要建一个自定义的引用类型,那要再写一个对应的List类来封装对应的引用对象数组么,答案是否定的,学编程我们都知道除了提高软件的性能外,还要写出健壮的优雅的代码,不要重复造轮子,增加代码复用。那么有一种选择就是直接使用Object作为类型,然后再做类型转换,还有可能出错。这时候进要引入泛型了,用泛型来定义一个模板,上述代码就是规定了List内部的数据类型为Integer也就是int的包装类,这里要注意,尖括号内的类型必须是引用类型,那么一个存放int的可变长数组建成了,如果要改为其他的类型,请修改尖括号内的class类名,请同步修改ArrayList处的类名。
编写泛型
编写一个使用泛型的类,首先要声明泛型,如下使用的是泛型T
class Father {
private String first;
private String last;
public Father(String first, String last) {
this.first = first;
this.last = last;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
}
//使用泛型
class Father<T> {
private T first;
private T last;
public Father(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
/*注意静态方法不能这么写
public static Father<T> set(T first, T last){
return new Father<T>(first, last);
}*/
/*这就没问题了,但是注意这里声明的T和原来的T不是同一个类型,也可以直接换成别的类型(E),这里我的理解是这里静态方法与
实例方法的区别,静态无法得到确切的类型转换信息,那么泛型的目的是安全的类型转换而不是直接输出一个Object,
编译器就不让它通过,但是如果我们在方法的返回值前面声明了一个泛型的类型,由于是静态方法而与实例中的类型隔绝了,
两者就算是同样命名,依然是不同的类型,当然我们也可以在成员方法里加这个泛型类型声明的标志,作为类型转换*/
public static <T> Father<T> set(T first, T last){
return new Father<T>(first, last);
}
//这样使用也是没问题的,但是一般不推荐这样使用
public <E> E cover(T t){
return (E)t;
}
}
但是泛型不一定就是使用T来做代表,也可以使用别的,比如Map里面会看见的K和V,但是一般都会使用带有一定含义的,比如前面的就分别代表key和value,还有E,element,U,usage之类的
类型擦除
这个概念很容易理解,上面说到一种直接使用Object的方案,事实上,泛型也是这么做的,当然没有那么简单,在编译及运行的时候都会做一些特殊处理,甚至还可以根据类型关系,限制使用的类型,这就是模板的作用。
顾名思义,擦除就是把代码写的类型擦除,如下:
//编写的java代码
class Try<T>{
T x;
}
//jvm编译器看见的代码
class Try<Object>{
Object x;
}
编译器会把泛型T看做Object,但是需要转型时会做安全的强制的类型转换,一般如果类型使用错误在编译期就会出错。
因为这个特性,泛型也有一些局限
//比如 Try<Integer>和Try<String>这两个类的class实例是一样的即:
new Try<Integer>().getClass() == new Try<String>.getClass() == Try.class
//还有上面提到的类型必须填写引用对象而不能使用基本类型也是因为这个,基本类型不是继承自Object
//由于无法确定类型也无法比较带泛型的实例
Try<Integer> t = new Try<Integer>();
if(t instanceof Try<String>.Class)
System.out.println('y');//通过打印出y
//也不能在class里实例泛型
class Try<T>{
T x;
Try(){
x = new T();//error
}
/*上面的代码经过擦拭后:
x = new Obejct();
*/
//借助反射可以这样实例一个泛型
Try(Class<t> class){
x = class.newInstance();
}
}
//还有注意可能会有意外的覆写方法,比如我们要覆写继承自Obejct的equals方法,这样写就不对
public boolean equals(T t){
return this == t;
}
//擦除后T会变成Object很明显这就变成覆写了,编译器会报错不允许运行,这时候改个方法名就好了
//泛型继承
class Father<T> {
private T first;
private T last;
public Father(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T t){
first = t;
}
public void setLast(T t){
last = t;
}
}
class Son extends Father<Integer> {
public Son (Integer first, Integer last) {
super(first, last);
}
//这里Son类显式的继承了父类的Integer类型,所以这里是可以通过反射来获取泛型的类型的
extends通配符
这里使用上面的Father为例子学习,由于我们使用了泛型,在使用Father类时必须要声明类型,但是如果我们写一个方法处理这个类的数据,但是为了安全性考虑我们的方法是有限制的
public static void main(String args[]){
Father<Number> f = new Father<Number>(1,2);//注意这里是没有问题的
handle(f);//success
Father<Integer> fa = new Father<Integer>(3,4);
handle(fa);//编译无法通过,因为类型不符,或者说,Father<Integer>不是Father<Number>的子类
}
//这个方法就限制了我们只处理Number类型的数据
static void handle(Father<Number> f){
f.getFirst();
}
这里就要引入一个extends通配符,修改一下
//这样就相当于告诉编译器,这个方法接收的是泛型为Number及其子类的Father,那上面的方法就可以编译通过了
static void handle(Father<? extends Number> f){
f.getFirst();
}
super通配符
上面的extends规范了类型的上限,同样的可以规范类型的下限,就是使用super通配符
//修改方法
static void handle(Father<? super Integer> f){
f.setFirst(1);
}
/*
* 此处限定了传入的Father的泛型类型必须是Integer,或Integer的父类
* */
<?>
也可以写作Father<?>,这个时候要注意它是所有Father类型的超类
读写规则
前面大概介绍了几种通配符的作用,但是没有明确指出他们之间的差别,最根本的差别就在限定范围之后的读写数据的问题。
总结一些博客的介绍描述我个人的一些理解:
extends:可读不可写,也就是无法使用Father类里面的setter方法,不管以何种形式输入都是无法编译的,举上面的例子,<? extends Number>,这就是上面我只展示了getter的原因,这里规定了T必须是Number的子类,我说它是一个上限,但是Number有不少的子类,各子类之间是无法转换的,如果我们定义一个Father<Integer>,那我们在setter传递一个Double也是合法的,因为Double也是Number的子类,但是Integer和Double是无法转换的,为了避免这种错误,jvm规定他不能使用泛型setter,当然不用指定类型的setter是没有问题的,可是不要忘了去掉泛型的setter,而且不推荐 这种用法除非特殊环境下,我认为因为这破坏了使用这个通配符的初衷,至于为什么可读,都是Number的子类,getter获取一个Number当然是没有问题的。
super:可写不可读,同上,但是这里的读也不是非说不能读,还是看例子,<? super Integer>,规定T必须是Integer或其父类,那Father<Object>,Father<Number>都是可以传入方法的,先说读,不可读我个人是理解为不可读为Integer,这是针对例子的说法,getter读出来的是一个Object类型,我们知道可以进行类型转换,但是我们要遵守这个规定,而且大量的转换也是有性能损坏的,还有同样也是违背了使用的初衷,那么写,实际上写只支持写入Integer类型的,这个我是这样理解的,就像上面说的在类型推断的时候我们jvm不希望出任何差错能安全的进行强制类型转换,那么我们这里最明确的类型就是Integer,所以只能传入Integer或其子类作为数据修改。
还有一个特例<?>,它是不可读也不可写的,当然结合上面的例子,可以发现其实还是可以读的,只是读出来的是一个Obejct对象,所以一般使用用途就是判空
PECS
PECS规则全名是:Producer Extends, Consumer Super。很明显就是生产者使用extends,消费者使用super
- Producer extends: 如果我们需要一个 List 提供类型为 T 的数据(即希望从 List 中读取 T 类型的数据), 那么我们需要使用 ? extends T, 例如 List<? extends Integer>. 但是我们不能向这个 List 添加数据.
- Consumer Super: 如果我们需要一个 List 来消费 T 类型的数据(即希望将 T 类型的数据写入 List 中), 那么我们需要使用 ? super T, 例如 List<? super Integer>. 但是这个 List 不能保证从它读取的数据的类型.