先来一道经典的测试题。
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
输出是啥?正确答案是true。上面的代码中涉及到了泛型,而输出的结果缘由是类型擦除。
1.泛型是什么?
泛型的英文是 generics,较为准确的说法就是为了参数化类型,或者说可以将类型当作参数传递给一个类或者是方法。
那么,如何解释类型参数化呢?
比如我们需要在Cache里面可以存取任何值,通常我们会这么做
public class Cache {
Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
但是这么做缺点就是每次getValue以后必须要强制转化才行.但是如果用泛型的话就是另外的画面
public class Cache<T> {
T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
测试代码
Cache<String> cache1 = new Cache<String>();
cache1.setValue("aaa");
// cache1.setValue(1); 这么写会报错,默认类型只能是string
String aString = cache1.getValue();
这就是泛型,它将 value 这个属性的类型也参数化了,这就是所谓的参数化类型
所以,综合上面信息,我们可以得到下面的结论。
- 1.与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力
- 2.当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制
- 3.泛型提高了程序代码的可读性,在定义或者实例化阶段,因为 Cache<String>这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。
当然,并不意味着 cache1.setValue(1) 这种操作完全不能执行,后面会说明
2.通配符 ?
在讲类型擦除前先介绍一下通配符 ?
除了用 <T>表示泛型外,还有 <?>这种形式。?被称为通配符。
class Base{}
class Sub extends Base{}
Sub sub = new Sub();
Base base = sub;
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;
最后一行代码成立吗?编译会通过吗?答案是否定的。
编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub>和 List<Base>有继承关系。
但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
所以,通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式。
- <?>被称作无限定的通配符。
- <? extends T>被称作有上限的通配符。
- <? super T>被称作有下限的通配符。
2.1无限定通配符 <?>
无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。
ArrayList<?> list = new ArrayList<>();
list.size();
list.isEmpty();
list.get(0);
list.add("aa")//这句要报错
<?>提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空
如果你看到分函数的参数有无限定通配符的list,而你此时又在查bug,那么这个分函数可以直接跳过,因为这个函数里面,这个list只读,不会对其进行修改
2.2有上限的通配符<? extends T>
主要特征是add受限
List<? extends Number> list = null;//list类型是Number子类,一定能get到数据
list = new ArrayList<Integer>();
// list.add(new Integer(1));//报错,因为list不能确定实例化的对象具体类型导致add()方法受限
list.get(0);//类型是Number,和无限定通配符的区别就是返回值的类型,无限定通配符返回object
2.3有下限的通配符<? super T>
主要特征是get受限
List<? super Integer> list = null;//list类型是Integer父类,一定能add int型数据
list = new ArrayList<Number>();
list.add(1);
Number number = list.get(0);//报错,get获取到的值不确定
3.类型擦除
回顾文章开始时的那段代码,打印的结果为 true 是因为 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。
为啥?这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
那么类型 String 和 Integer 怎么办?答案是泛型转译
还是以上面Cache<T>为例,看一下Cache类型
Cache<String> cache = new Cache<>();
System.out.println(cache.getClass());
Field[] fs = cache.getClass().getDeclaredFields();
for ( Field f:fs) {
System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}
打印的结果是Cache和Object
明显,并不是 Cache<T>这种形式,同时所谓的T也不是我们所想要的String.为什么呢?我们有必要来看一下Cache<T>的class文件
MAC下选中对应的Cache,然后cmd+shift+r就可以查看字节码
// Signature: <T:Ljava/lang/Object;>Ljava/lang/Object;
public class fanxing.Cache {
// Field descriptor #6 Ljava/lang/Object;
// Signature: TT;
java.lang.Object value;
// Method descriptor #10 ()V
// Stack: 1, Locals: 1
public Cache();
// Method descriptor #21 ()Ljava/lang/Object;
// Signature: ()TT;
// Stack: 1, Locals: 1
public java.lang.Object getValue();
// Method descriptor #26 (Ljava/lang/Object;)V
// Signature: (TT;)V
// Stack: 2, Locals: 2
public void setValue(java.lang.Object value);
}
看到了没?getValue返回的是Object,无论T是什么,字节码都是Object,进入JVM自然都是Object咯
顺带说一句,前文提到 cache1.setValue(1) 可以执行的奥秘就在这里
那怎么样获取T的具体类型呢?改一下,改成Cache<T extends String>.再看一下字节码
// Signature: <T:Ljava/lang/String;>Ljava/lang/Object;
public class fanxing.Cache {
// Field descriptor #6 Ljava/lang/String;
// Signature: TT;
java.lang.String value;
// Method descriptor #10 ()V
// Stack: 1, Locals: 1
public Cache();
// Method descriptor #21 ()Ljava/lang/String;
// Signature: ()TT;
// Stack: 1, Locals: 1
public java.lang.String getValue();
// Method descriptor #26 (Ljava/lang/String;)V
// Signature: (TT;)V
// Stack: 2, Locals: 2
public void setValue(java.lang.String value);
}
getValue返回string,Signature也变了
我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限。
(字节码中的descriptor表示返回值,Signature表示泛型信息)
4.类型擦除带来的局限性
- 利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制
- 当泛型遇见重载
4.1反射
还是Cache<T>
Cache<String> cache = new Cache<>();
cache.setValue("aaa");
// cache.setValue(123);正常不允许
try {
Method method = cache.getClass().getDeclaredMethod("setValue", Object.class);// 字节码是object
method.invoke(cache, 123);
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(cache.getValue());//这句话会报错,int无法转化为string,内部已经存在123这个int值,但无法输出
4.2重载
一下4个方法能同时存在么?
public void method(List<String> list) {
}
public void method(List<Integer> list) {
}
public int method(List<String> list) {
return 0;
}
public String method(List<Integer> list) {
return "";
}
先看一下字节码
// Method descriptor #19 (Ljava/util/List;)V
// Signature: (Ljava/util/List<Ljava/lang/String;>;)V
// Stack: 0, Locals: 2
public void method(java.util.List list);
// Method descriptor #19 (Ljava/util/List;)V
// Signature: (Ljava/util/List<Ljava/lang/Integer;>;)V
// Stack: 0, Locals: 2
public void method(java.util.List list);
// Method descriptor #19 (Ljava/util/List;)I
// Signature: (Ljava/util/List<Ljava/lang/String;>;)I
// Stack: 1, Locals: 2
public int method(java.util.List list);
// Method descriptor #19 (Ljava/util/List;)Ljava/lang/String;
// Signature: (Ljava/util/List<Ljava/lang/Integer;>;)Ljava/lang/String;
// Stack: 1, Locals: 2
public java.lang.String method(java.util.List list);
方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择.但是在 Class 文件格式之中,只要描述符不是完全一致的两个方法就可以共存。
从方法重载的要求来看,看Signature一行,首先把返回值去掉,然后类型擦除,整个Signature就剩下相同的Ljava/util/List,所以以上四个方法都不能共存
但是在class文件格式中,3和4的Method descriptor不同,导致在低版本的jdk里面可以共存.后两个方法jdk1.6是警告,jdk1.8更严格,直接爆红(警告和爆红的文字信息都是一样的)
参考文章
https://blog.csdn.net/briblue/article/details/76736356
https://blog.csdn.net/itmyhome1990/article/details/78872403
网友评论