java泛型
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
为什么要引入泛型?
在此之前举一个例子:在java 1.5之前,我想实现一个string数组or集合,并且它是可以动态的改变大小的,在这里会有很多解决方案,我这里用ArrayList去解决这个问题,但是在java 1.5之前,ArrayList的实现大概是下面这样的:
public ArrayList {
public Object get(int i){...}
public void add(Object obj) {...}
}
从代码可以看出来,在1.5之前,ArrayList获取某个元素是返回Object这个对象的,当我们需要获取之前我想实现的那个String集合的某个元素时,我需要在get方法拿到对象后进行强制转换,ok,好像没什么问题。但是当我给这个String集合add一个int 类型对象的时候,编译器是可以通过的,但是当进行强制转换时就会出错了。所以1.5后引入泛型是为了在编码时出现这种没必要的错误,即声明ArrayList后可以指定String类型,当你调用add方法时,参数不是String类型的编译工具是无法通过的。
进入正题
泛型类
所谓泛型类,就是我们在定义一个类时可以指定一个或多个类型参数,例如:
public Test<K,V> {
private K key ;
private V value;
public Test(){}
public Test(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) {
this.key = key ;
}
public K getKey(){
return key;
}
public void setValue(V value) {
this.value= value;
}
public V getValue(){
return value;
}
}
在定义Test类时,我们给其指定了2个类型参数 K、V,放在类名的尖括号里。类型参数的名称可以随意其,但是为了规范,最好是指定有意义的名称,例如 T(Type)、E(element)、K(key)、V(value)等。实例化泛型类的时候,我们只需要把类型参数换成具体的类型即可,如:
Test<String,Object> test = new Test<String,Object>();
泛型方法
简单来说就是带有类型参数的方法,泛型方法既可以定义在泛型类中,也可以定义在普通类中,看下面这段代码:
public <T> T getFirtElement(T[] tArray){
if(tArray == null) return null;
return tArray[0];
}
定义的格式是类型变量(T)放在修饰符的后面、返回类型的前面。需要注意的是,这里的类型不能是基本类型。在这个方法中,我们传入的参数是什么类型的数组,返回的也是该类型的对象,例如:
Integer[] array = {1,2};
Integer fir = getFirtElement(array);
类型变量的限定
在有些情况下,泛型类或者泛型方法想要对自己的类型参数进一步加一些限制。比如,我们想要限定类型参数只能为某个类的子类或者只能为实现了某个接口的类。相关的语法如下:
(BoundingType是一个类或者接口)。其中的BoundingType可以多于1个,用“&”连接即可。带extends关键字的表示子类限定,带super关键字的表示父类限定。关于限定的作用,我将在接下来的环节中进行一些例子讲解。
深入理解泛型实现原理
其实在java虚拟机中,是”不存在”泛型的概念的。比如我们上面定义的Test泛型类,在编译过后,在虚拟机的角度看,他是这样定义的:
public Test {
private Object key;
private Object value;
public Test(){}
public Test(Object key, Object value) {
this.key = key;
this.value = value;
}
public void setKey(Object key) {
this.key = key ;
}
public Object getKey(){
return key;
}
public void setValue(Object value) {
this.value= value;
}
public Object getValue(){
return value;
}
}
我们可以用命令就看出来,为什么在虚拟机角度,刚才定义的Test<K,V>
是这样的。先用javac 命令编译刚才的Test.java , 然后用javap -c -s Test.class 可以看到:
上面的这个类是通过类型擦除得到的。是Test类对应的原始类型(raw type)。类型擦除就是把所有类型参数替换为BoundingType(若未加限定就替换为Object)。例如我定义一个泛型的时候这样定义:
public MyList<T extends List>{...}
这个List就是限定,我限定了这个泛型参数必须是List类型。那么进行类型擦除时,编译器会把我的泛型方法的泛型类型强制转换为这个限定的类型,就是List类型。
在编译器的角度看,实际我们定义的Test类中的get方法是返回Object对象的,但是为什么编码的时候我们写的却是指定了类型的变量来接收呢?其实是编译器帮我们强制转换为我们要的类型了,我们可以这样理解编译器的工作:
V getValue(){
return (V) value;
}
这样看也行不够直观,我们可以通过下面这段代码来看看是否是这样:
public static void main(String[] args) {
Test<String,Integer> t= new Test<String,Integer>();
t.setKey("aaaa");
t.getKey();
}
javac 编译后,再javap -c -s 可得到:
我们可以看到,在18行,checkcast,也就是说编译器在检查类型转换的,我们可以很清楚的看到他要将返回的类型从Object转为String,说明确实是编译器帮我们完成了类型转换的工作。
方法的类型擦除会带来一些问题,例如这段代码:
public StringTest extends Test<String,String> {
public StringTest(){ super();}
public StringTest(String key, String value) {
super(key,value);
}
@override
public setValue(String value) {
if(value.compareTo(getKey()) > 0 ) {
super.setValue(value);
}
}
}
我们定义一个Test的子类,声明了限定的类型是String,String,并且重写了父类的setValue方法, 再看这段代码:
StringTest st = new StringTest();
Test<String,String> test = st;
test.setKey("aaa");
test.setValue("bbb");
在这段代码里,由于test实际上是引用了st这个对象,所以调用其setValue方法是,应该是调用子类的setValue方法,在这里,类型擦除和多态就会发生冲突,为什么会有冲突,我们可以这样理解:test在之前被声明为类型Test<String, String>
,该类在虚拟机看来只有一个”setValue(Object)”方法。因此在运行时,虚拟机发现test实际引用的是StringTest对象后,会去调用StringTest的setValue(Object)”,然而StringTest类中却只有”setValue(String)”方法。 解决这个问题的方法是由编译器在StringTest中生成一个桥方法 :
public void setValue(Object value){
setValue((String) value);
}
我们可以用javap -c -s 来验证一下:
我们可以很清楚的看到,在StringTest类中,他会生成一个重载的方法去解决这个多态的冲突,在setValue(Object)中,可以看到第2,5行,编译器将参数转换为String后,调用重载的setValue(String)方法,也就是我们刚才分析的那种情况。
由上面的分析,我们可以得知,在处理泛型的时候,编译器已经帮我们分担了一些类型转换的问题,类型参数问题,我们在编码的时候可以不用考虑这些问题。
类型通配符
在本文的上面有提到过类型限定,其实这是泛型的类型通配符。
假设我们有People这个父类,有name,age这两个属性变量,有对应的setter和getter方法。
我们又定义了一个Student类,这个类继承了People这个父类。假如我们有这样一个方法:
public void printName(Test<People,People> test) {
System.out.printIn(test.getValue().getName());
}
我们想打印一个人的名称,我们知道People和Student存在 “is-a”的关系,也就是继承,如果我们想打印某个student的名称,我们是否可以传入Test<Student,Student>
这样的类型的参数呢?上面这个方法显然是不行的,因为Test<Student,Student>
和 Test<People,People>
不存在”is-a”的关系,这是2种不同的类型,但是如果我们使用泛型的子类型限定通配符的话,我们就可以让这个方法接收这样类型的参数了。修改如下:
public void printName(Test<? extends People,? extends People> test) {
System.out.printIn(test.getValue().getName());
}
<? extends BoundingType>
的代码叫做通配符的子类型限定。
与之对应的还有通配符的超类型限定,格式是这样的:<? super BoundingType>
要注意的地方:
- 在使用子类型限定通配符时,setter方法是禁止的,因为编译器不知道形参究竟是什么类型(只知道是People的子类)