欧美在路边给了钱就可以做网站,包头seo,php做的网站打包成exe,做公司网站员工保险转载自 揭开Java 泛型类型擦除神秘面纱
泛型#xff0c;一个孤独的守门者。
大家可能会有疑问#xff0c;我为什么叫做泛型是一个守门者。这其实是我个人的看法而已#xff0c;我的意思是说泛型没有其看起来那么深不可测#xff0c;它并不神秘与神奇。泛型是 Java 中一…转载自 揭开Java 泛型类型擦除神秘面纱
泛型一个孤独的守门者。
大家可能会有疑问我为什么叫做泛型是一个守门者。这其实是我个人的看法而已我的意思是说泛型没有其看起来那么深不可测它并不神秘与神奇。泛型是 Java 中一个很小巧的概念但同时也是一个很容易让人迷惑的知识点它让人迷惑的地方在于它的许多表现有点违反直觉。
文章开始的地方先给大家奉上一道经典的测试题。
ListString l1 new ArrayListString();
ListInteger l2 new ArrayListInteger();System.out.println(l1.getClass() l2.getClass());
请问上面代码最终结果输出的是什么不了解泛型的和很熟悉泛型的同学应该能够答出来而对泛型有所了解但是了解不深入的同学可能会答错。
正确答案是 true。
上面的代码中涉及到了泛型而输出的结果缘由是类型擦除。先好好说说泛型。
泛型是什么
泛型的英文是 genericsgeneric 的意思是通用,而翻译成中文泛应该意为广泛型是类型。所以泛型就是能广泛适用的类型。
但泛型还有一种较为准确的说法就是为了参数化类型或者说可以将类型当作参数传递给一个类或者是方法。
那么如何解释类型参数化呢
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();
使用的方法也很简单只要我们做正确的强制转换就好了。
但是泛型却给我们带来了不一样的编程体验。
public class CacheT {T value;public Object getValue() {return value;}public void setValue(T value) {this.value value;}}
这就是泛型它将 value 这个属性的类型也参数化了这就是所谓的参数化类型。再看它的使用方法。
CacheString cache1 new CacheString();
cache1.setValue(123);
String value2 cache1.getValue();CacheInteger cache2 new CacheInteger();
cache2.setValue(456);
int value3 cache2.getValue();
最显而易见的好处就是它不再需要对取出来的结果进行强制转换了。但还有另外一点不同。 泛型除了可以将类型参数化外而参数一旦确定好如果类似不匹配编译器就不通过。 上面代码显示无法将一个 String 对象设置到 cache2 中因为泛型让它只接受 Integer 的类型。
所以综合上面信息我们可以得到下面的结论。 与普通的 Object 代替一切类型这样简单粗暴而言泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。 当具体的类型确定后泛型又提供了一种类型检测的机制只有相匹配的数据才能正常的赋值否则编译器就不通过。所以说它是一种类型安全检测机制一定程度上提高了软件的安全性防止出现低级的失误。 泛型提高了程序代码的可读性不必要等到运行的时候才去强制转换在定义或者实例化阶段因为 CacheString 这个类型显化的效果程序员能够一目了然猜测出代码要操作的数据类型。 下面的文章我们正常介绍泛型的相关知识。
泛型的定义和使用
泛型按照使用情况可以分为 3 种。 1. 泛型类。 2. 泛型方法。 3. 泛型接口。
泛型类
我们可以这样定义一个泛型类。
public class TestT {T field1;
}
尖括号 中的 T 被称作是类型参数用于指代任何类型。事实上T 只是一种习惯性写法如果你愿意。你可以这样写。
public class TestHello {Hello field1;
}
但出于规范的目的Java 还是建议我们用单个大写字母来代表类型参数。常见的如 1. T 代表一般的任何类。 2. E 代表 Element 的意思或者 Exception 异常的意思。 3. K 代表 Key 的意思。 4. V 代表 Value 的意思通常与 K 一起配合使用。 5. S 代表 Subtype 的意思文章后面部分会讲解示意。
如果一个类被 T 的形式定义那么它就被称为是泛型类。
那么对于泛型类怎么样使用呢
TestString test1 new Test();
TestInteger test2 new Test();
只要在对泛型类创建实例的时候在尖括号中赋值相应的类型便是。T 就会被替换成对应的类型如 String 或者是 Integer。你可以相像一下当一个泛型类被创建时内部自动扩展成下面的代码。
public class TestString {String field1;
}
当然泛型类不至接受一个类型参数它还可以这样接受多个类型参数。
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 被称为类型参数而方法中的 T 被称为参数化类型它不是运行时真正的参数。
当然声明的类型参数其实也是可以当作返回值的类型的。
public T T testMethod1(T t){return null;
} 泛型类与泛型方法的共存现象
public class Test1T{public void testMethod(T t){System.out.println(t.getClass().getName());}public T T testMethod1(T t){return t;}
}
上面代码中Test1T 是泛型类testMethod 是泛型类中的普通方法而 testMethod1 是一个泛型方法。而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的泛型方法始终以自己定义的类型参数为准。
所以针对上面的代码我们可以这样编写测试代码。
Test1String t new Test1();
t.testMethod(generic);
Integer i t.testMethod1(new Integer(1));
泛型类的实际类型参数是 String而传递给泛型方法的类型参数是 Integer两者不想干。
但是为了避免混淆如果在一个泛型类中存在泛型方法那么两者的类型参数最好不要同名。比如Test1T 代码可以更改为这样
public class Test1T{public void testMethod(T t){System.out.println(t.getClass().getName());}public E E testMethod1(E e){return e;}
} 泛型接口
泛型接口和泛型类差不多所以一笔带过。
public interface IterableT {
} 通配符
除了用 T 表示泛型外还有 ? 这种形式。 被称为通配符。
可能有同学会想已经有了 T 的形式了为什么还要引进 ? 这样的概念呢
class Base{}class Sub extends Base{}Sub sub new Sub();
Base base sub;
上面代码显示Base 是 Sub 的父类它们之间是继承关系所以 Sub 的实例可以给一个 Base 引用赋值那么
ListSub lsub new ArrayList();
ListBase lbase lsub;
最后一行代码成立吗编译会通过吗
答案是否定的。
编译器不会让它通过的。Sub 是 Base 的子类不代表 ListSub 和 ListBase 有继承关系。
但是在现实编码中确实有这样的需求希望泛型能够处理某一范围内的数据类型比如某个类和它的子类对此 Java 引入了通配符这个概念。
所以通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式。 ? 被称作无限定的通配符。 ? extends T 被称作有上限的通配符。 ? super T 被称作有下限的通配符。 无限定通配符
public void testWildCards(Collection? collection){
}
上面的代码中方法内的参数是被无限定通配符修饰的 Collection 对象它隐略地表达了一个意图或者可以说是限定那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型因为它是未知的。所以你只能调用 Collection 中与类型无关的方法。 我们可以看到当 ? 存在时Collection 对象丧失了 add() 方法的功能编译器不通过。 我们再看代码。
List? wildlist new ArrayListString();
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
这个和 ? extends T 相对应代表 T 及 T 的超类。
public void testSuper(Collection? super Sub para){
}
? super 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(CollectionT collection){}
取代。
值得注意的是如果用泛型方法来取代通配符那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。
public T void test(CollectionT collection){collection.add((T)new Integer(12));collection.add((T)123);
}
需要特别注意的是类型参数适用于参数之间的类别依赖关系举例说明。
public class Test2 T,E extends T{T value1;E value2;
}
public D,S extends D void test(D d,S s){}
E 类型是 T 类型的子类显然这种情况类型参数更适合。 有一种情况是通配符和类型参数一起使用。
public T void test(T t,Collection? extends T collection){}
如果一个方法的返回类型依赖于参数的类型那么通配符也无能为力。
public T test1(T t){return value1;
} 类型擦除
泛型是 Java 1.5 版本才引进的概念在这之前是没有泛型的概念的但显然泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为泛型信息只存在于代码编译阶段在进入 JVM 之前与泛型相关的信息会被擦除掉专业术语叫做类型擦除。
通俗地讲泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码
ListString l1 new ArrayListString();
ListInteger l2 new ArrayListInteger();System.out.println(l1.getClass() l2.getClass());
打印的结果为 true 是因为 ListString 和 ListInteger 在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了。
可能同学会问那么类型 String 和 Integer 怎么办
答案是泛型转译。
public class Erasure T{T object;public Erasure(T object) {this.object object;}}
Erasure 是一个泛型类我们查看它在运行时的状态信息可以通过反射。
ErasureString erasure new ErasureString(hello);
Class eclz erasure.getClass();
System.out.println(erasure class is:eclz.getName());
打印的结果是
erasure class is:com.frank.test.Erasure
Class 的类型仍然是 Erasure 并不是 ErasureT 这种形式那我们再看看泛型类中 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。
ErasureString erasure new ErasureString(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 类型了。
类型擦除带来的局限性
类型擦除是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除它会抹掉很多继承相关的特性这是它带来的局限性。
理解类型擦除有利于我们绕过开发当中可能遇到的雷区同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如 正常情况下因为泛型的限制编译器不让最后一行代码编译通过因为类似不匹配但是基于对类型擦除的了解利用反射我们可以绕过这个限制。
public interface ListE extends CollectionE{boolean add(E e);
}
上面是 List 和其中的 add() 方法的源码定义。
因为 E 代表任意的类型所以类型擦除时add 方法其实等同于
boolean add(Object obj);
那么利用反射我们绕过编译器去调用 add 方法。
public class ToolTest {public static void main(String[] args) {ListInteger ls new ArrayList();ls.add(23);
// ls.add(text);try {Method method ls.getClass().getDeclaredMethod(add,Object.class);method.invoke(ls,test);method.invoke(ls,42.9f);} catch (NoSuchMethodException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (SecurityException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IllegalAccessException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IllegalArgumentException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (InvocationTargetException e) {// TODO Auto-generated catch blocke.printStackTrace();}for ( Object o: ls){System.out.println(o);}}}
打印结果是
23
test
42.9
可以看到利用类型擦除的原理用反射的手段就绕过了正常开发中编译器不允许的操作限制。
泛型中值得注意的地方
泛型类或者泛型方法中不接受 8 种基本数据类型。
所以你没有办法进行这样的编码。
Listint li new ArrayList();
Listboolean li new ArrayList();
需要使用它们对应的包装类。
ListInteger li new ArrayList();
ListBoolean li1 new ArrayList(); 对泛型方法的困惑
public T T test(T t){return null;
}
有的同学可能对于连续的两个 T 感到困惑其实 T 是为了说明类型参数是声明,而后面的不带尖括号的 T 是方法的返回值类型。 你可以相像一下如果 test() 这样被调用
test(123);
那么实际上相当于
public String test(String t); Java 不能创建具体类型的泛型数组
这句话可能难以理解代码说明。
ListInteger[] li2 new ArrayListInteger[];
ListBoolean li3 new ArrayListBoolean[];
这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。
ListInteger 和 ListBoolean 在 jvm 中等同于ListObject 所有的类型信息都被擦除程序也无法分辨一个数组中的元素类型具体是 ListInteger类型还是 ListBoolean 类型。
但是
List?[] li3 new ArrayList?[10];
li3[1] new ArrayListString();
List? v li3[1];
借助于无限定通配符却可以前面讲过 代表未知类型所以它涉及的操作都基本上与类型无关因此 jvm 不需要针对它对类型作判断因此它能编译通过但是只提供了数组中的元素因为通配符原因它只能读不能写。比如上面的 v 这个局部变量它只能进行 get() 操作不能进行 add() 操作这个在前面通配符的内容小节中已经讲过。 泛型并不神奇
我们可以看到泛型其实并没有什么神奇的地方泛型代码能做的非泛型代码也能做。
而类型擦除是泛型能够与之前的 java 版本代码兼容共存的原因。
可量也正因为类型擦除导致了一些隐患与局限。
但我还是要建议大家使用泛型如官方文档所说的如果可以使用泛型的地方尽量使用泛型。
毕竟它抽离了数据类型与代码逻辑本意是提高程序代码的简洁性和可读性并提供可能的编译时类型转换安全检测功能。
类型擦除不是泛型的全部但是它却能很好地检测我们对于泛型这个概念的理解程度。
我在文章开头将泛型比作是一个守门人原因就是他本意是好的守护我们的代码安全然后在门牌上写着出入的各项规定及“xxx 禁止出入”的提醒。但是同我们日常所遇到的那些门卫一般他们古怪偏执死板守旧我们可以利用反射基于类型擦除的认识来绕过泛型中某些限制现实生活中也总会有调皮捣蛋者能够基于对门卫们生活作息的规律选择性地绕开他们的监视另辟蹊径溜进或者溜出大门然后扬长而去剩下守卫者一个孤独的身影。