关于泛型擦除的知识(来源于csdn地址:https://blog.csdn.net/briblue/article/details/76736356)

泛型,一个孤独的守门者。java

你们可能会有疑问,我为何叫作泛型是一个守门者。这实际上是我我的的见解而已,个人意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇。泛型是 Java 中一个很小巧的概念,但同时也是一个很容易让人迷惑的知识点,它让人迷惑的地方在于它的许多表现有点违反直觉。程序员

文章开始的地方,先给你们奉上一道经典的测试题。编程

List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass()); 
  • 1
  • 2
  • 3
  • 4
  • 5

请问,上面代码最终结果输出的是什么?不了解泛型的和很熟悉泛型的同窗应该可以答出来,而对泛型有所了解,可是了解不深刻的同窗可能会答错。数组

正确答案是 true。安全

上面的代码中涉及到了泛型,而输出的结果原因是类型擦除。先好好说说泛型。jvm

泛型是什么?

泛型的英文是 generics,generic 的意思是通用,而翻译成中文,泛应该意为普遍,型是类型。因此泛型就是能普遍适用的类型。测试

但泛型还有一种较为准确的说法就是为了参数化类型,或者说能够将类型看成参数传递给一个类或者是方法。this

那么,如何解释类型参数化呢?编码

public class Cache { Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

假设 Cache 可以存取任何类型的值,因而,咱们能够这样使用它。spa

Cache cache = new Cache(); cache.setValue(134); int value = (int) cache.getValue(); cache.setValue("hello"); String value1 = (String) cache.getValue();
  • 1
  • 2
  • 3
  • 4
  • 5

使用的方法也很简单,只要咱们作正确的强制转换就行了。

可是,泛型却给咱们带来了不同的编程体验。

public class Cache<T> { T value; public Object getValue() { return value; } public void setValue(T value) { this.value = value; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这就是泛型,它将 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(); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

最显而易见的好处就是它再也不须要对取出来的结果进行强制转换了。但,还有另一点不一样。
这里写图片描述
泛型除了能够将类型参数化外,而参数一旦肯定好,若是相似不匹配,编译器就不经过。
上面代码显示,没法将一个 String 对象设置到 cache2 中,由于泛型让它只接受 Integer 的类型。

因此,综合上面信息,咱们能够获得下面的结论。

  1. 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别能够像参数同样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
  2. 当具体的类型肯定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,不然编译器就不经过。因此说,它是一种类型安全检测机制,必定程度上提升了软件的安全性防止出现低级的失误。
  3. 泛型提升了程序代码的可读性,没必要要等到运行的时候才去强制转换,在定义或者实例化阶段,由于 Cache<String> 这个类型显化的效果,程序员可以一目了然猜想出代码要操做的数据类型。

下面的文章,咱们正常介绍泛型的相关知识。

泛型的定义和使用

泛型按照使用状况能够分为 3 种。
1. 泛型类。
2. 泛型方法。
3. 泛型接口。

泛型类

咱们能够这样定义一个泛型类。

public class Test<T> { T field1; }
  • 1
  • 2
  • 3

尖括号 <> 中的 T 被称做是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法,若是你愿意。你能够这样写。

public class Test<Hello> { Hello field1; }
  • 1
  • 2
  • 3

但出于规范的目的,Java 仍是建议咱们用单个大写字母来表明类型参数。常见的如:
1. T 表明通常的任何类。
2. E 表明 Element 的意思,或者 Exception 异常的意思。
3. K 表明 Key 的意思。
4. V 表明 Value 的意思,一般与 K 一块儿配合使用。
5. S 表明 Subtype 的意思,文章后面部分会讲解示意。

若是一个类被 <T> 的形式定义,那么它就被称为是泛型类。

那么对于泛型类怎么样使用呢?

Test<String> test1 = new Test<>(); Test<Integer> test2 = new Test<>(); 
  • 1
  • 2
  • 3

只要在对泛型类建立实例的时候,在尖括号中赋值相应的类型即是。T 就会被替换成对应的类型,如 String 或者是 Integer。你能够相像一下,当一个泛型类被建立时,内部自动扩展成下面的代码。

public class Test<String> { String field1; } 
  • 1
  • 2
  • 3
  • 4
  • 5

固然,泛型类不至接受一个类型参数,它还能够这样接受多个类型参数。

public class MultiType <E,T>{ E value1; T value2; public E getValue1(){ return value1; } public T getValue2(){ return value2; } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

泛型方法

public class Test1 { public <T> void testMethod(T t){ } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

泛型方法与泛型类稍有不一样的地方是,类型参数也就是尖括号那一部分是写在返回值前面的。<T> 中的 T 被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。

固然,声明的类型参数,其实也是能够看成返回值的类型的。

public <T> T testMethod1(T t){ return null; }
  • 1
  • 2
  • 3

泛型类与泛型方法的共存现象

public class Test1<T>{ public void testMethod(T t){ System.out.println(t.getClass().getName()); } public <T> T testMethod1(T t){ return t; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面代码中,Test1<T> 是泛型类,testMethod 是泛型类中的普通方法,而 testMethod1 是一个泛型方法。而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以本身定义的类型参数为准

因此,针对上面的代码,咱们能够这样编写测试代码。

Test1<String> t = new Test1(); t.testMethod("generic"); Integer i = t.testMethod1(new Integer(1)); 
  • 1
  • 2
  • 3
  • 4

泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,二者不想干。

可是,为了不混淆,若是在一个泛型类中存在泛型方法,那么二者的类型参数最好不要同名。好比,Test1<T> 代码能够更改成这样

public class Test1<T>{ public void testMethod(T t){ System.out.println(t.getClass().getName()); } public <E> E testMethod1(E e){ return e; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

泛型接口

泛型接口和泛型类差很少,因此一笔带过。

public interface Iterable<T> { }
  • 1
  • 2

通配符 ?

除了用 <T> 表示泛型外,还有 <?> 这种形式。 被称为通配符。

可能有同窗会想,已经有了 <T> 的形式了,为何还要引进 <?> 这样的概念呢?

class Base{}

class Sub extends Base{}

Sub sub = new Sub(); Base base = sub; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,因此 Sub 的实例能够给一个 Base 引用赋值,那么

List<Sub> lsub = new ArrayList<>(); List<Base> lbase = lsub; 
  • 1
  • 2
  • 3
  • 4

最后一行代码成立吗?编译会经过吗?

答案是否认的。

编译器不会让它经过的。Sub 是 Base 的子类,不表明 List<Sub>List<Base> 有继承关系。

可是,在现实编码中,确实有这样的需求,但愿泛型可以处理某一范围内的数据类型,好比某个类和它的子类,对此 Java 引入了通配符这个概念。

因此,通配符的出现是为了指定泛型中的类型范围

通配符有 3 种形式。

  1. <?> 被称做无限定的通配符。
  2. <? extends T> 被称做有上限的通配符。
  3. <? super T> 被称做有下限的通配符。

无限定通配符

public void testWildCards(Collection<?> collection){ }
  • 1
  • 2

上面的代码中,方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者能够说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,由于它是未知的。因此,你只能调用 Collection 中与类型无关的方法。
这里写图片描述

咱们能够看到,当 <?> 存在时,Collection 对象丧失了 add() 方法的功能,编译器不经过。
咱们再看代码。

List<?> wildlist = new ArrayList<String>(); wildlist.add(123);// 编译不经过
  • 1
  • 2

有人说,<?> 提供了只读的功能,也就是它删减了增长具体类型元素的能力,只保留与具体类型无关的功能。它无论装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空?我想这种需求仍是很常见的吧。

有同窗可能会想,<?> 既然做用这么眇小,那么为何还要引用它呢? 

我的认为,提升了代码的可读性,程序员看到这段代码时,就可以迅速对此创建极简洁的印象,可以快速推断源码做者的意图。

<? extends T>

<?> 表明着类型未知,可是咱们的确须要对于类型的描述再精确一点,咱们但愿在一个范围内肯定类别,好比类型 A 及 类型 A 的子类均可以。

 

public void testSub(Collection<? extends Base> para){ }
  • 1
  • 2
  • 3

上面代码中,para 这个 Collection 接受 Base 及 Base 的子类的类型。

可是,它仍然丧失了写操做的能力。也就是说

para.add(new Sub()); para.add(new Base());
  • 1
  • 2

仍然编译不经过。

没有关系,咱们不知道具体类型,可是咱们至少清楚了类型的范围。

<? super T>

这个和 <? extends T> 相对应,表明 T 及 T 的超类。

public void testSuper(Collection<? super Sub> para){ }
  • 1
  • 2

<? super T> 神奇的地方在于,它拥有必定程度的写操做的能力。

public void testSuper(Collection<? super Sub> para){ para.add(new Sub());//编译经过 para.add(new Base());//编译不经过 }
  • 1
  • 2
  • 3
  • 4

通配符与类型参数的区别

通常而言,通配符能干的事情均可以用类型参数替换。
好比

public void testWildCards(Collection<?> collection){} 
  • 1
  • 2

能够被

public <T> void test(Collection<T> collection){}
  • 1

取代。

值得注意的是,若是用泛型方法来取代通配符,那么上面代码中 collection 是可以进行写操做的。只不过要进行强制转换。

public <T> void test(Collection<T> collection){ collection.add((T)new Integer(12)); collection.add((T)"123"); }
  • 1
  • 2
  • 3
  • 4

须要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。

public class Test2 <T,E extends T>{ T value1; E value2; } 
  • 1
  • 2
  • 3
  • 4
  • 5
public <D,S extends D> void test(D d,S s){ }
  • 1
  • 2
  • 3

E 类型是 T 类型的子类,显然这种状况类型参数更适合。
有一种状况是,通配符和类型参数一块儿使用。

public <T> void test(T t,Collection<? extends T> collection){ }
  • 1
  • 2
  • 3

若是一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。

public T test1(T t){ return value1; }
  • 1
  • 2
  • 3

类型擦除

泛型是 Java 1.5 版本才引进的概念,在这以前是没有泛型的概念的,但显然,泛型代码可以很好地和以前版本的代码很好地兼容。

这是由于,泛型信息只存在于代码编译阶段,在进入 JVM 以前,与泛型相关的信息会被擦除掉,专业术语叫作类型擦除

通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码

List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass());
  • 1
  • 2
  • 3
  • 4

打印的结果为 true 是由于 List<String>List<Integer> 在 jvm 中的 Class 都是 List.class。

泛型信息被擦除了。

可能同窗会问,那么类型 String 和 Integer 怎么办?

答案是泛型转译。

public class Erasure <T>{ T object; public Erasure(T object) { this.object = object; } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Erasure 是一个泛型类,咱们查看它在运行时的状态信息能够经过反射。

Erasure<String> erasure = new Erasure<String>("hello"); Class eclz = erasure.getClass(); System.out.println("erasure class is:"+eclz.getName()); 
  • 1
  • 2
  • 3
  • 4

打印的结果是

erasure class is:com.frank.test.Erasure
  • 1

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()); }
  • 1
  • 2
  • 3
  • 4

打印结果是

Field name object type:java.lang.Object
  • 1

那咱们可不能够说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?

这种说法,不彻底正确。

咱们更改一下代码。

public class Erasure <T extends String>{ // public class Erasure <T>{ T object; public Erasure(T object) { this.object = object; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如今再看测试结果:

Field name object type:java.lang.String
  • 1

咱们如今能够下结论了,在泛型类被类型擦除的时候,以前泛型类中的类型参数部分若是没有指定上限,如 <T> 则会被转译成普通的 Object 类型,若是指定了上限如 <T extends String> 则类型参数就被替换成类型上限。

因此,在反射中。

public class Erasure <T>{ T object; public Erasure(T object) { this.object = object; } public void add(T object){ } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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()); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

打印结果是

method:public void com.frank.test.Erasure.add(java.lang.Object)
  • 1

也就是说,若是你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod("add",Object.class) 不然程序会报错,提示没有这么一个方法,缘由就是类型擦除的时候,T 被替换成 Object 类型了。

类型擦除带来的局限性

类型擦除,是泛型可以与以前的 java 版本代码兼容共存的缘由。但也由于类型擦除,它会抹掉不少继承相关的特性,这是它带来的局限性。

理解类型擦除有利于咱们绕过开发当中可能遇到的雷区,一样理解类型擦除也能让咱们绕过泛型自己的一些限制。好比
这里写图片描述

正常状况下,由于泛型的限制,编译器不让最后一行代码编译经过,由于相似不匹配,可是,基于对类型擦除的了解,利用反射,咱们能够绕过这个限制。

public interface List<E> extends Collection<E>{ boolean add(E e); } 
  • 1
  • 2
  • 3
  • 4
  • 5

上面是 List 和其中的 add() 方法的源码定义。

由于 E 表明任意的类型,因此类型擦除时,add 方法其实等同于

boolean add(Object obj);
  • 1

那么,利用反射,咱们绕过编译器去调用 add 方法。

public class ToolTest { public static void main(String[] args) { List<Integer> 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 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(); } for ( Object o: ls){ System.out.println(o); } } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

打印结果是:

23
test
42.9
  • 1
  • 2
  • 3

能够看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不容许的操做限制。

泛型中值得注意的地方

泛型类或者泛型方法中,不接受 8 种基本数据类型。

因此,你没有办法进行这样的编码。

List<int> li = new ArrayList<>(); List<boolean> li = new ArrayList<>(); 
  • 1
  • 2
  • 3

须要使用它们对应的包装类。

List<Integer> li = new ArrayList<>(); List<Boolean> li1 = new ArrayList<>();
  • 1
  • 2

对泛型方法的困惑

public <T> T test(T t){ return null; } 
  • 1
  • 2
  • 3
  • 4

有的同窗可能对于连续的两个 T 感到困惑,其实 <T> 是为了说明类型参数,是声明,然后面的不带尖括号的 T 是方法的返回值类型。
你能够相像一下,若是 test() 这样被调用

test("123");
  • 1

那么实际上至关于

public String test(String t);
  • 1

Java 不能建立具体类型的泛型数组

这句话可能难以理解,代码说明。

List<Integer>[] li2 = new ArrayList<Integer>[]; List<Boolean> li3 = new ArrayList<Boolean>[]; 
  • 1
  • 2
  • 3

这两行代码是没法在编译器中编译经过的。缘由仍是类型擦除带来的影响。

List 和 List 在 jvm 中等同于 List ,全部的类型信息都被擦除,程序也没法分辨一个数组中的元素类型具体是 List 类型仍是 List 类型。

可是,

List<?>[] li3 = new ArrayList<?>[10]; li3[1] = new ArrayList<String>(); List<?> v = li3[1];
  • 1
  • 2
  • 3

借助于无限定通配符却能够,前面讲过 表明未知类型,因此它涉及的操做都基本上与类型无关,所以 jvm 不须要针对它对类型做判断,所以它能编译经过,可是,只提供了数组中的元素由于通配符缘由,它只能读,不能写。好比,上面的 v 这个局部变量,它只能进行 get() 操做,不能进行 add() 操做,这个在前面通配符的内容小节中已经讲过。

泛型,并不神奇

咱们能够看到,泛型其实并无什么神奇的地方,泛型代码能作的非泛型代码也能作。

而类型擦除,是泛型可以与以前的 java 版本代码兼容共存的缘由。

可量也正由于类型擦除致使了一些隐患与局限。

但,我仍是要建议你们使用泛型,如官方文档所说的,若是可使用泛型的地方,尽可能使用泛型。

毕竟它抽离了数据类型与代码逻辑,本意是提升程序代码的简洁性和可读性,并提供可能的编译时类型转换安全检测功能。

类型擦除不是泛型的所有,可是它却能很好地检测咱们对于泛型这个概念的理解程度。

我在文章开头将泛型比做是一个守门人,缘由就是他本意是好的,守护咱们的代码安全,而后在门牌上写着出入的各项规定,及“xxx 禁止出入”的提醒。可是同咱们平常所遇到的那些门卫通常,他们古怪偏执,死板守旧,咱们能够利用反射基于类型擦除的认识,来绕过泛型中某些限制,现实生活中,也总会有调皮捣蛋者可以基于对门卫们生活做息的规律,选择性地绕开他们的监视,另辟蹊径溜进或者溜出大门,而后扬长而去,剩下守卫者一个孤独的身影。

因此,我说泛型,并不神秘,也不神奇

相关文章
相关标签/搜索