先看一段代码的输出
public static void main(String[] args){ List<String> list1=new ArrayList<String>(); List<Integer> list2=new ArrayList<Integer>(); System.out.println(list1.getClass()==list2.getClass()); }
结果为true。
为什么? 这里两个List均用到泛型,而泛型在编译的时候通过类型擦除已经没有了类型一说。所有两个class是相等的。
泛型是什么?
泛型一种较为准确的说法就是为了参数化类型,或者说可以将类型当做参数传递给一个类或者方法。
怎么理解参数化?
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();
使用起来比较麻烦,每次使用均要做强制转换;
但是如果使用范围呢?前面讲到泛型可以将类型当做一个参数传递给一个类或者方法;这时在定义类的时候,就加上一个参数传递,即<T>
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();
使用的时候将类型String,或者Integer传递过去就好了,这样就不会出现强制转换的错误了。
但是一旦定义好了类型后,如果类型不匹配,编译器就不会通过了。如下,List定义的String类型,但是传递的缺失Integer类型。
所以:
- 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
- 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
- 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为
Cache<String>
这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。
那有没有办法绕过泛型类型检测机制?可以,我们可以看到List的add方法源码
里面还是一个泛型,也就是说add方法并没有指定具体的类型,那么我们可以通过反射的方式调用add方法,如下:
List<String> list1=new ArrayList<String>(); Method method = list1.getClass().getDeclaredMethod("add",Object.class); method.invoke(list1,123); method.invoke(list1,123.98); for ( Object o: list1){ System.out.println(o); }
输出结果:
123
123.98
int和double类型均可以输出。
泛型类或者泛型方法中,不接受 8 种基本数据类型。
所以,你没有办法进行这样的编码。
List<int> li = new ArrayList<>();
List<boolean> li = new ArrayList<>();
需要使用它们对应的包装类。
List<Integer> li = new ArrayList<>();
List<Boolean> li1 = new ArrayList<>();
对泛型方法的困惑
public <T> T test(T t){
return null;
}
有的同学可能对于连续的两个 T 感到困惑,其实 <T>
是为了说明类型参数,是声明,而后面的不带尖括号的 T 是方法的返回值类型。
你可以相像一下,如果 test() 这样被调用
test("123");
那么实际上相当于
public String test(String t);
Java 不能创建具体类型的泛型数组
这句话可能难以理解,代码说明。
List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];
这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。
List 和 List 在 jvm 中等同于 List ,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List 类型还是 List 类型。
但是,
List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];
借助于无限定通配符却可以,前面讲过 ?
代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作,这个在前面通配符的内容小节中已经讲过。
除了用 <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>
有继承关系。
但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
所以,通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式。
<?>
被称作无限定的通配符。<? extends T>
被称作有上限的通配符。<? super T>
被称作有下限的通配符。
类型擦除
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
打印的结果为 true 是因为 List<String>
和 List<Integer>
在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了。
可能同学会问,那么类型 String 和 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.frank.test.Erasure
Class 的类型仍然是 Erasure 并不是 Erasure<T>
这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型。
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>
则类型参数就被替换成类型上限。
所以,在反射中。
public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
public void add(T object){
}
}
add() 这个方法对应的 Method 的签名应该是 Object.class。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
System.out.println(" method:"+m.toString());
}
打印结果是
method:public void com.frank.test.Erasure.add(java.lang.Object)
也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod("add",Object.class)
否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。即在反射中这么调用
Method tt1 = erasure.getClass().getDeclaredMethod("add",Object.class);
如果把Object变为String,则出现下面错误
Exception in thread "main" java.lang.NoSuchMethodException: test.Erasure.add(java.lang.String)
想不出错呢? 那就把Erasure限定上限为String类型
Erasure <T extends String>