泛型详解
今天跟着frank大神的blog 传送门学习泛型的知识。
首先看一下下面代码
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
上次在讲解反射机制的时候通过反射举了个例子和这个是有关的。
上面结果输出是true,而造成输出结果的原因是泛型擦除。
泛型的概念
泛型就是一句话:将类型参数化
。
那什么又是类型参数化呢?
举一个例子:
public class Cache {
Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
假设这个Cache可以存取任何的值,那我们可以这样利用它:
Cache cache = new Cache();
cache.setValue(134);
int value = (int) cache.getValue();
cache.setValue("hello");
String value1 = (String) cache.getValue();
使用起来很简单,我们只需要在get的时候强转就行了。
而泛型可以做到相同的功能,却带来了另外一种体验:
public class Cache<T> {
T value;
public Object getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
这里就是体现了泛型的作用,它将value这个类型参数化了。再来看看它的使用方法:
Cache<String> cache1 = new Cache<String>();
cache1.setValue("123");
String value2 = cache1.getValue();
Cache<Integer> cache2 = new Cache<Integer>();
cache2.setValue(456);
int value3 = cache2.getValue();
看来和上面的相比,它的好处就是不用强制转换类型了。
而当一个new出一个并确定好泛型后,就不能再放置别的类型了,比如上面的cache2,当我们对其setValue(“hello”)的时候,编译都不能通过。
从上可以总结:
- 相比于直接用Object这样代替一切类型而言,泛型更加符合面向抽象开发的软件编程宗旨,因为泛型可以使具体的数据类别都传入进去。
- 当泛型的具体类型确定以后,泛型提供了类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译无法通过。
- 提高了代码可读性
泛型的使用
泛型根据使用情况可以分为三种方法:
- 泛型类
- 泛型方法
- 泛型接口
泛型类
我们可以这样定义一个泛型类:
public class Test<T> {
T field1;
}
<>
尖括号中的T被称作是类型参数,用于指代任意类型,事实上,T只是一种习惯性的写法,我们甚至可以把T换成任意的字母,不过出于规范我们把单个大写字母来表示一些参数类型:
- T:表示任何类
- E:表示Element,或者Expection
- K:代表key
- V:代表value
- S:代表SubType,后面会讲到
泛型的使用,最前面已经讲解过一个Cache的例子了。
当然泛型不止接受一个类型参数,他还可以接受多个类型参数:
public class MultiType <E,T>{
E value1;
T value2;
public E getValue1(){
return value1;
}
public T getValue2(){
return value2;
}
}
泛型方法
public class Test1 {
public <T> void testMethod(T t){
}
}
泛型方法和泛型类不同之处在于,泛型方法中<>是写在返回值前的,而< T>中的T不是运行时真正的参数。
当然,声明的类型参数,也可以当成返回值类型返回的。
泛型方法和泛型类共存现象:
public class Test1<T>{
public void testMethod(T t){
System.out.println(t.getClass().getName());
}
public <T> T testMethod1(T t){
return t;
}
}
有人会问,泛型类中的参数T和泛型方法中的参数T有没有联系?
上述代码中 testMethod只是Test1中的一个普通方法,而testMethod1则是一个泛型方法。
泛型类的类型参数和泛型方法的类型参数没有任何的联系,泛型方法只以自己定义的类型参数为标准。
针对上面,可以写出这样的代码:
Test1<String> t = new Test1();
t.testMethod("generic");
Integer i = t.testMethod1(new Integer(1));
就是我们定义了一个泛型为String的泛型类,里面的泛型方法我们却可以传入Integer~
但是为了避免混淆,我们最好不要再泛型类中那样写,我们可以改成:
public class Test1<T>{
public void testMethod(T t){
System.out.println(t.getClass().getName());
}
public <E> E testMethod1(E e){
return e;
}
}
泛型接口
和泛型类差不多:
public interface InterfaceTest<T> {
}
通配符
除了<T>
外,还有<?>
,?通常被称为通配符。
为什么已经有了T,还要引进?这样的概念呢?
class Base{}
class Sub extends Base{}
Sub sub = new Sub();
Base base = sub;
上面代码显示,Base是Sub的父类,它们是继承的关系,所以Sub的实例可以给Base引用赋值,那么:
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;
这样的代码是合理、成立的吗?
答案是否定的,这样的代码不能通过编译。
因为虽然Sub虽然是Base的子类,但是不代表List< Sub>是List< Base>的子类呀。
实际开发中我们有时候会有这样的需求,而通配符的出现就是为了解决这个问题的。
通配符的出现是为了指定泛型中的类型范围。
通配符有3中类型:
-
<?>
被称作无限定的通配符 -
< ? extends T>
被称为有上限的通配符 -
< ? super T>
被称为有下限的通配符
无限定通配符<?>
无限定通配符经常与容器类 配合使用,它其中的?代表的是未知类型,所以涉及到了?操作时,一定与具体的类型无关。
public void testWildCards(Collection<?> collection){
}
这里代码表示,testWildCards无需关注collection的真实类型,因为其类型参数是位置的,所以你只能调用collection中与类型无关的方法。
我们可以知道当?修饰了Collection的参数时,Collection就丧失了add()的方法。编译器不能通过。
我们再来看看代码:
List<?> wildlist = new ArrayList<String>();
wildlist.add(123);// 编译不通过
通常认为<?>提供了只读功能,它删减了增删改具体数据的能力,只保留与具体类型无关的功能。比如它修饰容器时只关心容器的大小、元素的数量。
那<?>的作用这么弱,为什么我们还要引用它呢?
可能是为了提高代码的可读性吧。我们看到这种代码,就会建立起简洁的形象。
< ? extends T>
<?>中的?代表未知,但是我们的确需要对类型描述的再具体一点,对?描述的范围进行缩小,比如说它是A类或者A类的子类都行。public void testSub(Collection<? extends Base> para){
}
上面代码中,para这个Collection接受的是Base或者Base的子类。
但是它仍然丧失了写操作的能力,也就是说
para.add(new Sub());
para.add(new Base());
编译这样编译仍然不能通过。但是我们至少搞清楚了它要表示的范围。
< ? super T>
这个和前面的相对应,代表?是T或者T的超类(即父类)。
神奇的在于,它有一定的写操作能力。
public void testSuper(Collection<? super Sub> para){
para.add(new Sub());//编译通过
para.add(new Base());//编译不通过
}
通配符与类型参数的区别 一般而言,通配符能干的事情都可以用类型参数替换。
比如
public void testWildCards(Collection<?> collection){
}
可以被类型参数替换
public <T> void test(Collection<T> collection){
}
值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。
public <T> void test(Collection<T> collection){
collection.add((T)new Integer(12));
collection.add((T)"123");
}
需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明:
public class Test2 <T,E extends T>{
T value1;
E value2;
}
E 类型是 T 类型的子类,显然这种情况类型参数更适合。
有一种情况是,通配符和类型参数一起使用:
public <T> void test(T t,Collection<? extends T> collection){
}
类型擦除
之前在讲反射的时候提过一句话:
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
最后得到的结果都是是 List.class,他们的String、Integer都被擦除了。
那Stirng、Integer会怎么办?答案是泛型转译。
public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
}
Erasure是泛型类。我们查看它在运行时的状态信息:
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
打印的结果为:
erasure class is:com.test.Erasure
Class的类型仍然是Erasure而不是Erasure< T>,那我们再看看泛型类中T在运行时是什么类型:
Field[] fs = eclz.getDeclaredFields();
for ( Field f:fs) {
System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}
打印结果为:
Field name object type:java.lang.Object
那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成Object呢?答案是不完全正确的。
public class Erasure <T extends String>{
// public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
}
得到的结果为:
Field name object type:java.lang.String
结论就是,在泛型类被泛型擦除时,之前泛型类中的泛型参数部分如果没有指定上限,如
<T>
就会被转译成普通的Object类型,如果指定了上限如< T extends String>
,则类型参数就被替换成类型上限。
类型擦除带来的局限性
类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。
上述就是局限性的体现。
但是我们可以通过反射机制来解决这个问题。
泛型值得注意的地方
- 泛型不接受8中基础数据类型
- Java 不能创建具体类型的泛型数组
比如:
List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];
上述代码不能通过编译,但是通配符却可以解决:
List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];
借助于无限定通配符却可以,前面讲过 ?代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作,这个在前面通配符的内容小节中已经讲过。