当前位置: 首页 > 科技观察

浅谈Java泛型及其实现

时间:2023-03-19 18:42:12 科技观察

泛型基础泛型是Java语言类型系统的扩展,有点类似于C++模板,其中类型参数可以看作是使用参数化类型时指定的类型占位符。泛型的引入是对Java语言的重大增强,并带来了许多好处:类型安全。类型错误现在在编译时捕获,而不是在运行时显示为java.lang.ClassCastException,将类型检查从运行时移到编译时有助于开发人员更容易地发现错误并提高程序性能可靠性消除了代码中的许多强制类型转换,增强了代码的可读性,并带来了更大优化的可能性。通用类型是什么不会影响对象实例的类型。因此,通过改变泛型的方式来尝试定义不同的重载方法是不允许的。剩下的内容,泛型的使用我就不多说了。泛型的通配符等知识请自行查阅。在进入下面的讨论之前,我想问几个问题:定义一个泛型类时会生成多少个类,比如ArrayList有多少个类定义了一个泛型方法,最后会有多少个方法?为什么泛型参数不能是类文件中的基本类型?ArrayList是一个类吗?ArrayList和List和ArrayList和List之间有什么关系?这些类型之间可以引用赋值吗?类型擦除正确理解泛型概念的第一个前提是理解类型擦除。Java中的泛型基本上是在编译器级别实现的。泛型中的类型信息不包含在生成的Java字节码中。使用泛型时加入的类型参数,在编译时会被编译器去掉。这个过程称为类型擦除。代码中定义的List、List等类型,编译后会变成List。JVM看到的只是List,泛型附加的类型信息对JVM是不可见的。Java编译器在编译过程中会尝试寻找可能的错误,但仍然无法避免在运行时发生类型转换异常。类型擦除也是Java的泛型实现方式与C++的模板机制实现方式的一个重要区别。泛型的许多奇怪特性都与这种类型擦除的存在有关,包括:泛型类没有自己唯一的Class对象。例如,没有List.class或List.class,只有List.class。静态变量由泛型类的所有实例共享。对于声明为MyClass的类,访问其中静态变量的方法仍然是MyClass.myStaticVar。无论newMyClass还是newMyClass创建的对象都共享一个静态变量。Java异常处理的catch语句中不能使用泛型类型参数。因为异常处理是由JVM在运行时执行的。由于类型信息被擦除,JVM无法区分MyException和MyException这两种异常类型。对于JVM,它们都是MyException类型。也无法执行异常对应的catch语句。类型擦除的基本过程也比较简单。首先,找到用于替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上限,则使用该上限。用具体类替换代码中的所有类型参数。同时去掉出现的类型声明,也就是去掉<>的内容。例如,Tget()方法声明变为Objectget();List变为List。泛型的实现原理由于种种原因,Java无法实现真正??的泛型,只能通过类型擦除来实现伪泛型。虽然不会有类型扩展(C++模板的麻烦问题),但也产生了很多新的问题。所以Sun在这些问题上做了很多限制,防止我们犯各种错误。保证类型安全首先是泛型声明的类型安全。既然抹去了类型,那如何保证只能使用泛型变量限定的类型呢?java编译器编译时首先检查代码中的泛型类型,然后进行类型擦除。该类型检查对象是谁?我们先来看一个例子。ArrayListarrayList1=newArrayList();//正确,只能放StringArrayListarrayList2=newArrayList();//可以放任何Object所以不会报错,但是编译时会有警告。但是,第一种情况,可以达到和使用泛型参数一样的效果,而第二种情况,则完全没有效果。因为,最初类型检查是在编译时完成的。newArrayList()只是在内存中开辟了一个存储空间,可以存储任意类型的对象。真正涉及类型检查的是它的引用,因为我们用它来引用arrayList1来调用它的方法,比如调用add()方法。所以arrayList1引用可以完成泛型类型检查。对arrayList2的引用没有使用泛型,所以它不会起作用。类型检查是为了参考。谁是引用,谁就用这个引用调用一个泛型方法,然后对这个引用调用的方法进行类型检测,而不管它实际引用的是什么对象。实现自动类型转换由于类型擦除,所有泛型类型变量将被替换为原始类型。这就产生了一个问题,既然都是替换成原来的类型,为什么我们获取到的时候不需要进行强制类型转换呢?publicclassTest{publicstaticvoidmain(String[]args){ArrayListlist=newArrayList();list.add(newDate());DatemyDate=list.get(0);}}编译器调用泛型方法后,会在返回调用点之前添加类型转换操作。比如上面的get函数,就是在get方法完成之后,跳转回原来赋值操作的指令位置之前,加上一个强制转换。的类型由编译器推断。我们先来看一个泛型中继承关系的例子:method,父类的类型是Object,而子类的类型是Date,参数类型不同。如果这是在普通的继承关系中,那根本就不是重写,而是重载。publicvoidsetValue(java.util.Date);//我们重写的setValue方法Code:0:aload_01:aload_12:invokespecial#16//invokeAsetValue:(Ljava/lang/Object;)V5:returnpublicjava.util.DategetValue();//我们重写的getValue方法Code:0:aload_01:invokespecial#23//A.getValue:()Ljava/lang/Object;4:checkcast#267:areturnpublicjava.lang.ObjectgetValue();//编译生成的方法通过编译器Code:0:aload_01:invokevirtual#28//MethodgetValue:()来调用我们重写的getValue方法;4:areturnpublicvoidsetValue(java.lang.Object);//编译器在编译时生成的方法Code:0:aload_01:aload_12:checkcast#265:invokevirtual#30//MethodsetValue;来调用我们重写的setValue方法)V8:return还有,可能会有疑问,子类中的方法ObjectgetValue()和DategetValue()是同时存在的,但是如果是两个常规方法,它们的方法签名是一样,也就是说,虚拟机根本无法区分这两种方法。如果我们自己写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机是允许的,因为虚拟机是通过参数类型和返回类型来决定一个方法的,所以编译器为了实现通用的类型多态允许你做这个看似“违法”的事情,然后交给虚拟机去分辨。让我们看另一个经常出现的例子。classA{Objectget(){returnnewObject();}}classBextendsA{@OverrideIntegerget(){returnnewInteger(1);}}publicstaticvoidmain(String[]args){Aa=newB();Bb=(B)a;Ac=newA();a.get();b.get();c.get();}反编译后的结果17:invokespecial#5//Methodcom/suemi/network/test/A."":()V20:astore_321:aload_122:invokevirtual#6//Methodcom/suemi/network/test/A.get:()Ljava/lang/Object;25:pop26:aload_227:invokevirtual#7//Methodcom/suemi/network/test/B.get:()Ljava/lang/Integer;30:pop31:aload_332:invokevirtual#6//Methodcom/suemi/network/test/A.get:()Ljava/lang/Object;其实当我们用parent类引用调用子类的get时,会先调用JVM生成的override方法,然后在bridge方法中调用自己写的方法。泛型参数的继承关系在Java中,大家比较熟悉的就是通过继承机制产生的类型架构。例如,String继承自Object。根据Liskov替换原则,子类可以替换父类。当需要引用Object类时,传入一个String对象是没有问题的。但在相反的情况下,即当子类引用被替换为父类引用时,就需要进行强制类型转换。编译器不能保证转换在运行时是合法的。这种子类替换父类的自动类型转换机制同样适用于数组。String[]可以替代Object[]。但是泛型的引入对这种类型系统产生了一定的影响。如前所述,List不能替代List。引入泛型后的类型系统增加了两个维度:一个是类型参数本身的继承结构,另一个是泛型??类或接口本身的继承结构。第一种是指List和List的情况,类型参数String继承自Object。第二种是指继承自Collection接口的List接口。对于这种类型系统,有如下一些规则:具有相同类型参数的泛型类之间的关系取决于泛型类本身的继承结构。即List可以赋值给Collection类型引用,List可以替代Collection。这也适用于具有上限和下限的类型声明。当在泛型类的类型声明中使用通配符时,这个替换的判断可以在两个维度上展开。例如,对于集合,用来代替它的引用可以在Collection的维度上展开,即List和Set等;也可以在Number层面进行扩展,即Collection和Collection等,如此循环往复,ArrayList和HashSet也可以替代Collection。如果泛型类包含多个类型参数,则上述规则分别适用于每个类型参数。理解了以上规则后,就可以轻松修改实例分析中给出的代码了。只需将List更改为List。List可以替代List的子类型,所以传参不会出错。个人认为在上面的情况下用子类型这个词来描述这种关系是不合适的,因为List本质上不能算是一个类型,而是在List类型上增加了编译器检查约束。没有子类型这样的东西。只能通过赋值时是否可以进行类型转换来解释。使用泛型Runtime类型查询的注意点//错误,擦除类型后ArrayList只有原来的类型,泛型信息String不存在,所以无法判断if(arrayListinstanceofArrayList)if(arrayListinstanceofArrayList)//正确异常中使用泛型的问题不能抛出或捕获泛型类的对象。事实上,泛型类扩展Throwable是不合法的。为什么不能扩展Throwable,因为异常是在运行时捕获并抛出的,编译时会抹掉所有通用信息。擦除类型信息后,多处不同泛型参数的catch都变成了原来的类型Object,也就是说多处的catch变成了完全一样,这自然是不允许的。不能在catch子句中使用通用变量。publicstaticvoiddoWork(Classt){try{...}catch(Te){//编译错误T->Throwable,下面永远不会被捕获,所以不允许...}catch(IndexOutOfBoundse){}}不允许创建泛型类数组Pair[]table=newPair[10];//编译错误Pair[]table=newPair[10];//无编译错误因为数组必须携带自身元素的类型信息,所以类型擦除后,Pair数组变成了Pair数组,数组只能携带自身元素的信息元素是对。但是它不能携带其泛型参数类型的信息,所以无法保证table[i]赋值的类型安全。编译器只能禁用这种操作。泛型类中的静态方法和静态变量泛型类中的静态方法和静态变量不能使用泛型类声明的泛型类型参数。publicclassTest2{publicstaticTone;//编译错误publicstaticTshow(Tone){//编译错误returnnull;}}因为泛型类中泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法没有需要用一个对象来调用。对象没有创建,怎么判断这个泛型参数的类型,所以当然是错误的。类型擦除后冲突重定义classPair{publicbooleanequals(Tvalue){returnnull;}}方法,同时有两个equals(Objecto)。参考文章Java深度探险(五)——Java泛型Java语法糖(三):泛型Java泛型(二)、泛型内部原理:类型擦除及类型擦除带来的问题Java泛型:通配符的使用