参考:https://blog.csdn.net/xiangwanpeng/article/details/77896340
https://blog.csdn.net/s10461/article/details/53941091
本文的顺序是先讲泛型,然后讲关于类型擦除的问题。
泛型
概述
首先要解决的问题:什么是泛型?为什么要使用泛型?
这是网上一段内容的引用:
泛型,就是“参数化类型”。 我们在讲到参数的情况时,一般讲的都是在定义方法的时候定义的形参,然后在使用的时候传入实参。
那么”参数化类型“就很好理解了: 将类型由原来的具体类型进行参数化, 类似于方法之中的变量参数,将类型也定义成变量形式。只是在使用方法的时候传入具体的类型(类型实参)
泛型本质是参数化类型,在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。这种参数类型可以用在类型,接口和方法之中,分别称为泛型类,泛型接口和泛型方法。
举2个例子
下面两个例子的输出不同,下面会详细叙述其原因。
首先这个例子可以说明泛型的作用:
package GenericStudy;
import java.util.ArrayList;
import java.util.List;
public class GenerateTesting {
public static void main(String[] args) {
List arrayList=new ArrayList();
arrayList.add("abcd");
arrayList.add(1234);
for(int i =0;i<arrayList.size();i++){
String item = arrayList.get(i);
System.out.println("Testing generic: "+item);
}
}
}
输出为:
Testing generic: abcd
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer incompatible with java.lang.String
at GenericStudy.GenerateTesting.main(GenerateTesting.java:13)
首先,此处的错误的确显而易见:将arrayList之中第二个Integer对象cast成String,那么肯定是会出问题的。但是这处在编译阶段并没有报错。而为了对于的 List<> 这样类型的对象,希望可以在编译阶段就解决,那么泛型就应运而生了。
下面是我自己改过的代码:
package GenericStudy;
import java.util.ArrayList;
import java.util.List;
public class GenerateTesting {
public static void main(String[] args) {
List arrayList=new ArrayList();
arrayList.add("abcd");
arrayList.add(1234);
for(int i =0;i<arrayList.size();i++){
Object item = arrayList.get(i);
System.out.println("Testing generic: "+item);
}
}
}
结果是:
Testing generic: abcd
Testing generic: 1234
此处运行正常,原因是使用Object类来接住遍历的对象,然后对其直接进行操作。
特性
泛型只在编译阶段有效,下面的代码为例:
CheckArrayListType.java
package GenericStudy;
import java.util.ArrayList;
public class CheckArrayListType {
public static void main(String[] args) {
ArrayList<String> arrayList1 = new ArrayList<>();
arrayList1.add("abcde");
ArrayList<Integer> arrayList2 = new ArrayList<>();
arrayList2.add(12345);
ArrayList<Object> arrayList3 = new ArrayList<>();
arrayList3.add(123433);
arrayList3.add("hahahaha");
System.out.println(arrayList3);
/*
Here will show that below is always true
*/
System.out.println(arrayList1.getClass() == arrayList2.getClass());
}
}
在最下面这一行,会出现这样的提示:
程序在编译之后,会采取去泛型化的特征,也就是Java 之中所说的泛型,只在编译阶段有效。
在编译过程中,正确检验泛型结果之后,会将泛型相关的信息全部擦除,并且在对象进入和厉害方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
总结:泛型类型在逻辑上可以看成是多个不同的类型,实际都是相同的类型。
泛型的使用
泛型有三种使用方式:泛型类,泛型接口,泛型方法。
泛型类
泛型类型用于类的定义之中,被称为泛型类。通过泛型可以完成对 一组 类的操作对外开放相同的接口。最典型的就是各种容器类,比如 List,Set,Map。
基本写法:
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
}
下面是自己的几个例子:
GenericClassExample.java
package GenericStudy.GenericClass;
public class GenericClassExample<SB> {
private SB key;
public GenericClassExample(SB key){
this.key=key;
}
public SB getKey(){
return key;
}
}
在这个例子之中,我们首先定义了一个类型 SB,用来作为泛型的指定参数。
GenericClassTesting.java
package GenericStudy.GenericClass;
public class GenericClassTesting {
public static void main(String[] args) {
GenericClassExample<Integer> genericInteger = new GenericClassExample<Integer>(123);
GenericClassExample<String> genericString = new GenericClassExample<String>("Hello");
System.out.println("genericInteger is "+genericInteger.getKey());
System.out.println("genericString is "+genericString.getKey());
System.out.println("********Below is without real parameter of type*******");
GenericClassExample generic = new GenericClassExample(123);
GenericClassExample generic2 = new GenericClassExample("String");
System.out.println("Generic is "+generic.getKey());
System.out.println("Generic2 is "+generic2.getKey());
}
}
结果是:
genericInteger is 123
genericString is Hello
********Below is without real parameter of type*******
Generic is 123
Generic2 is String
在第二个例子之中,我们对两种类型分别做了对比,一种是指定传入的泛型类型 GenericClassExample<Integer>
和 GenericClassExample<String>
,一种直接使用泛型,没有对其具体的传入类型进行指定。下面的输出,可以看出两种情况都可以正常输出结果。但是第二种情况IDEA会有如下的提示:
其提示为:这里的使用方法是 unchecked call,IDEA默认会建议将其中的类型参数补上。
如果没有指定具体的传入类型,那么这里的泛型就没法起到真正的检查作用。其传入方法的类型可以为任何的类型。
注意:
- 泛型的类型参数只可以为类类型,不可以为简单类型。
- 不可以对确定的泛型类型做 instanceof 操作,比如下面的操作就是非法的,编译就会出错:
if(ex_num instanceof Generic<Number>){ }
泛型接口
泛型接口和泛型类的定义和使用基本相同。泛型接口经常被用在各种类的生产器之中,下面这个例子:
public interface GenericInterface<SB>{
public SB next();
}
定义了一个泛型接口。
当实现泛型接口的类没有传入泛型实参的时候:
在没有传入泛型实参的时候,和泛型类的定义相同,在声明类的时候,要把泛型的声明也一起加到类之中。
例如:class GenericClassImpl<SB> implements GenericInterface<SB>
如果不声明泛型,那么编译器会报错:”Unknown class”
下面是例子:
package GenericStudy.GenericClass;
public class GenericClassImpl<SB> implements GenericInterface<SB> {
@Override
public SB next(){
return null;
}
}
当实现泛型接口的类,传入泛型实参的时候,要将所有的泛型类型都替换成具体的泛型实参。比如我们以 SB 作为泛型类型,那么要在 implement 的过程之中将所有的 SB 都换成 String或者其他要使用的类型。
package GenericStudy.GenericClass;
import java.util.Random;
public class ClassUseGenericInterface implements GenericInterface<String> {
private String[] fruits = new String[]{"Apple","Banana","Pear"};
@Override
public String next() {
Random rand=new Random();
return fruits[rand.nextInt(3)];
}
}
泛型通配符(Wildcard)
package GenericStudy.GenericClass;
public class GenericClassTesting {
public static void main(String[] args) {
GenericClassExample<Integer> genericInteger = new GenericClassExample<Integer>(123);
GenericClassExample<String> genericString = new GenericClassExample<String>("Hello");
System.out.println("genericInteger is " + genericInteger.getKey());
System.out.println("genericString is " + genericString.getKey());
System.out.println("********Below is without real parameter of type*******");
GenericClassExample generic = new GenericClassExample(123);
GenericClassExample generic2 = new GenericClassExample("String");
System.out.println("Generic is " + generic.getKey());
System.out.println("Generic2 is " + generic2.getKey());
System.out.println("*********Below is Wildcard example***************");
GenericClassExample<Integer> gInteger = new GenericClassExample<>(123);
GenericClassExample<Number> gNumber = new GenericClassExample<>(456);
showValue(gInteger);
}
public static void showValue(GenericClassExample<Number> obj){
System.out.println("Key value is "+obj.getKey());
}
}
编译之后会返回给我们下面的信息:
Error:(22, 19) java: incompatible types: GenericStudy.GenericClass.GenericClassExample<java.lang.Integer> cannot be converted to GenericStudy.GenericClass.GenericClassExample<java.lang.Number>
提示信息是其不可以被直接转换,那么可以看出,不同版本的泛型类实例是互不兼容的。
那么如何解决这个办法呢?毕竟如果为了Integer的情况再写一个 showValue(GenericClassExample<Integer> obj)
,那么与java的多态理念是相悖的。因此,我们需要一个在逻辑上面可以同时表示Generic<Integer>
和 Generic<Number>
父类的引用类型,由此,类型通配符应运而生。
将上面的方法略改一下:
public static void showValue(GenericClassExample<?> obj){
System.out.println("Key value is "+obj.getKey());
}
此处的类型通配符,一般是使用 ? 来代替具体的类型实参。注意,此处’?’ 是类型实参,而不是类型形参! 直白一些说,此处的 ‘?’ 就是和 Number, String 等等一样的实际类型,可以将其看做是所有类型的父型。
当操作类型时,不需要使用类型的具体功能时,只使用Object类之中的功能,那么就可以用 ? 通配符来表示未知类型。
泛型方法
首先一句话总结:泛型方法是在调用方法的时候指明泛型的具体类型的方法。
总结一下,所有需要使用的泛型都需要在<>
之中进行声明,以下面的泛型方法举例:
public static <SB> SB genericMethod(Class<SB> sbClass) throws InstantiationException, IllegalAccessException {
SB instance = sbClass.newInstance();
return instance;
}
在使用这个泛型方法的时候,首先在<SB>
之中声明了此方法之中涉及的泛型变量,然后在返回类型和传入类型之中都加入了这个<SB>
。
泛型类之中使用了泛型的成员方法并不是泛型方法。
下面是自己的例子和输出。
package GenericStudy.GenericClass;
public class GenericClassTesting {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
GenericClassExample<Integer> genericInteger = new GenericClassExample<Integer>(123);
GenericClassExample<String> genericString = new GenericClassExample<String>("Hello");
System.out.println("genericInteger is " + genericInteger.getKey());
System.out.println("genericString is " + genericString.getKey());
System.out.println("********Below is without real parameter of type*******");
GenericClassExample generic = new GenericClassExample(123);
GenericClassExample generic2 = new GenericClassExample("String");
System.out.println("Generic is " + generic.getKey());
System.out.println("Generic2 is " + generic2.getKey());
System.out.println("*********Below is Wildcard example***************");
GenericClassExample<Integer> gInteger = new GenericClassExample<>(123);
GenericClassExample<Number> gNumber = new GenericClassExample<>(456);
showValue(gInteger);
System.out.println("************");
// GenericClassExample gExample = new GenericClassExample();
Object obj = genericMethod(Class.forName("GenericStudy.GenericClass.GenericClassImpl"));
System.out.println(obj);
}
public static void showValue(GenericClassExample<?> obj) {
System.out.println("Key value is " + obj.getKey());
}
public static <SB> SB genericMethod(Class<SB> sbClass) throws InstantiationException, IllegalAccessException {
SB instance = sbClass.newInstance();
return instance;
}
}
输出为:
genericInteger is 123
genericString is Hello
********Below is without real parameter of type*******
Generic is 123
Generic2 is String
*********Below is Wildcard example***************
Key value is 123
************
GenericStudy.GenericClass.GenericClassImpl@ce3af2b1
Process finished with exit code 0
类型擦除
Java的泛型都是伪泛型,这是什么意思呢?因为在编译期间,所有的泛型信息都会被直接擦除掉。那么就涉及到了我们这一章的标题:类型擦除
具体证明在我们之前的:
之中所看到的代码标黄的部分。
如果我们这个地方使用两个类的 getClass 方法,那么我们也可以发现其结果为 true,这样意味着泛型都被擦除掉了,最后剩下的只是原始类型。
下面是个例子:
package GenericStudy.GenericClass;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
public class Test4 {
public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, InvocationTargetException {
ArrayList<Integer> arrayList3 = new ArrayList<Integer>();
arrayList3.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i = 0; i < arrayList3.size(); i++) {
System.out.println(arrayList3.get(i));
}
}
}
结果为:
1
asd
可见此处使用反射,我们可以在已经指定了泛型类型为 Integer 的 List 之中赋入 String。
这说明了Integer泛型在编译之后已经擦除了,只剩下了原始类型。
类型擦除之后的原始类型
首先我们要知道:什么是原始类型?
原始类型,是擦除了泛型信息,最后在字节码之中的类型变量的真实类型。
比如将所有泛型替换成 Object 之后的类。
在编译的时候进行检查的注意事项
public class Test10 {
public static void main(String[] args) {
//
ArrayList<String> arrayList1=new ArrayList();
arrayList1.add("1");//编译通过
arrayList1.add(1);//编译错误
String str1=arrayList1.get(0);//返回类型就是String
ArrayList arrayList2=new ArrayList<String>();
arrayList2.add("1");//编译通过
arrayList2.add(1);//编译通过
Object object=arrayList2.get(0);//返回类型就是Object
new ArrayList<String>().add("11");//编译通过
new ArrayList<String>().add(22);//编译错误
String string=new ArrayList<String>().get(0);//返回类型就是String
}
}
上面这个例子就是编译时候的检查方式。解释如下:
类型检查,是在编译的时候完成的。new ArrayList()
只是在内存之中开辟一个内存空间而已,其内部可以存储各种类型的对象,真正有类型检查的是其引用,也就是等号左边的部分,因为我们是使用等号左边的部分,比如arrayList1 来进行的方法调用,所以 arrayList1 引用就可以完成泛型类型的检查。但是arrayList2 就没有使用泛型,所以不可以。
上面这个例子,我们就可以看出来,类型检查就是针对引用的,而无关其真正引用的对象。