Java 泛型总结(一):基本用法与类型擦除
简介
Java在1.5引入了泛型机制,泛型本质是参数化类型,也就是说变量的类型是一个参数,在使用时再指定为具体类型。泛型可以用于类、接口、方法,通过使用泛型可以使代码更简单、安全。然而Java中的泛型使用了类型擦除,所以只是伪泛型。这篇文章对泛型的使用以及存在的问题做个总结,主要参考自《Java编程思想》。
这个系列的另外两篇文章:
- Java泛型总结(二):泛型与数组
- Java泛型总结(三):通配符的使用
基本用法
泛型类
如果有一个类Holder用于包装一个变量,这个变量的类型可能是任意的,怎么编写Holder呢?在没有泛型之前可以这样:
publicclassHolder1{ privateObjecta; publicHolder1(Objecta){ this.a=a; } publicvoidset(Objecta){ this.a=a; } publicObjectget(){ returna; } publicstaticvoidmain(String[]args){ Holder1holder1=newHolder1("notGeneric"); Strings=(String)holder1.get(); holder1.set(1); Integerx=(Integer)holder1.get(); } }
在Holder1中,有一个用Object引用的变量。因为任何类型都可以向上转型为Object,所以这个Holder可以接受任何类型。在取出的时候Holder只知道它保存的是一个Object对象,所以要强制转换为对应的类型。在main方法中,holder1先是保存了一个字符串,也就是String对象,接着又变为保存一个Integer对象(参数1会自动装箱)。从Holder中取出变量时强制转换已经比较麻烦,这里还要记住不同的类型,要是转错了就会出现运行时异常。
下面看看Holder的泛型版本:
publicclassHolder2{ privateTa; publicHolder2(Ta){ this.a=a; } publicTget(){ returna; } publicvoidset(Ta){ this.a=a; } publicstaticvoidmain(String[]args){ Holder2 holder2=newHolder2<>("Generic"); Strings=holder2.get(); holder2.set("test"); holder2.set(1);//无法编译参数1不是String类型 } }
在Holder2中,变量a是一个参数化类型T,T只是一个标识,用其它字母也是可以的。创建Holder2对象的时候,在尖括号中传入了参数T的类型,那么在这个对象中,所有出现T的地方相当于都用String替换了。现在的get的取出来的不是Object,而是String对象,因此不需要类型转换。另外,当调用set时,只能传入String类型,否则编译无法通过。这就保证了holder2中的类型安全,避免由于不小心传入错误的类型。
通过上面的例子可以看出泛使得代码更简便、安全。引入泛型之后,Java库的一些类,比如常用的容器类也被改写为支持泛型,我们使用的时候都会传入参数类型,如:ArrayList
泛型方法
泛型不仅可以针对类,还可以单独使某个方法是泛型的,举个例子:
publicclassGenericMethod{ publicvoidf(Kk,Vv){ System.out.println(k.getClass().getSimpleName()); System.out.println(v.getClass().getSimpleName()); } publicstaticvoidmain(String[]args){ GenericMethodgm=newGenericMethod(); gm.f(newInteger(0),newString("generic")); } } 代码输出: Integer String
GenericMethod类本身不是泛型的,创建它的对象的时候不需要传入泛型参数,但是它的方法f是泛型方法。在返回类型之前是它的参数标识
调用泛型方法时可以不显式传入泛型参数,上面的调用就没有。这是因为编译器会使用参数类型推断,根据传入的实参的类型(这里是integer和String)推断出K和V的类型。
类型擦除
什么是类型擦除
Java的泛型使用了类型擦除机制,这个引来了很大的争议,以至于Java的泛型功能受到限制,只能说是”伪泛型“。什么叫类型擦除呢?简单的说就是,类型参数只存在于编译期,在运行时,Java的虚拟机(JVM)并不知道泛型的存在。先看个例子:
publicclassErasedTypeEquivalence{ publicstaticvoidmain(String[]args){ Classc1=newArrayList().getClass(); Classc2=newArrayList ().getClass(); System.out.println(c1==c2); } }
上面的代码有两个不同的ArrayList:ArrayList
泛型参数会擦除到它的第一个边界,比如说上面的Holder2类,参数类型是一个单独的T,那么就擦除到Object,相当于所有出现T的地方都用Object替换。所以在JVM看来,保存的变量a还是Object类型。之所以取出来自动就是我们传入的参数类型,这是因为编译器在编译生成的字节码文件中插入了类型转换的代码,不需要我们手动转型了。如果参数类型有边界那么就擦除到它的第一个边界,这个下一节再说。
擦除带来的问题
擦除会出现一些问题,下面是一个例子:
classHasF{ publicvoidf(){ System.out.println("HasF.f()"); } } publicclassManipulator{ privateTobj; publicManipulator(Tobj){ this.obj=obj; } publicvoidmanipulate(){ obj.f();//无法编译找不到符号f() } publicstaticvoidmain(String[]args){ HasFhasF=newHasF(); Manipulator manipulator=newManipulator<>(hasF); manipulator.manipulate(); } }
上面的Manipulator是一个泛型类,内部用一个泛型化的变量obj,在manipulate方法中,调用了obj的方法f(),但是这行代码无法编译。因为类型擦除,编译器不确定obj是否有f()方法。解决这个问题的方法是给T一个边界:
classManipulator2{ privateTobj; publicManipulator2(Tx){obj=x;} publicvoidmanipulate(){obj.f();} }
现在T的类型是
地方都用HasF替换。这样编译器就知道obj是有方法f()的。
但是这样就抵消了泛型带来的好处,上面的类完全可以改成这样:
classManipulator3{ privateHasFobj; publicManipulator3(HasFx){obj=x;} publicvoidmanipulate(){obj.f();} }
所以泛型只有在比较复杂的类中才体现出作用。但是像
classReturnGenericType{ privateTobj; publicReturnGenericType(Tx){obj=x;} publicTget(){returnobj;} }
这里的get()方法返回的是泛型参数的准确类型,而不是HasF。
类型擦除的补偿
类型擦除导致泛型丧失了一些功能,任何在运行期需要知道确切类型的代码都无法工作。比如下面的例子:
publicclassErased{ privatefinalintSIZE=100; publicstaticvoidf(Objectarg){ if(arginstanceofT){}//Error Tvar=newT();//Error T[]array=newT[SIZE];//Error T[]array=(T)newObject[SIZE];//Uncheckedwarning } }
通过newT()创建对象是不行的,一是由于类型擦除,二是由于编译器不知道T是否有默认的构造器。一种解决的办法是传递一个工厂对象并且通过它创建新的实例。
interfaceFactoryI{ Tcreate(); } classFoo2 { privateTx; public >Foo2(Ffactory){ x=factory.create(); } //... } classIntegerFactoryimplementsFactoryI { publicIntegercreate(){ returnnewInteger(0); } } classWidget{ publicstaticclassFactoryimplementsFactoryI { publicWidgetcreate(){ returnnewWidget(); } } } publicclassFactoryConstraint{ publicstaticvoidmain(String[]args){ newFoo2 (newIntegerFactory()); newFoo2 (newWidget.Factory()); } }
另一种解决的方法是利用模板设计模式:
abstractclassGenericWithCreate{ finalTelement; GenericWithCreate(){element=create();} abstractTcreate(); } classX{} classCreatorextendsGenericWithCreate { Xcreate(){returnnewX();} voidf(){ System.out.println(element.getClass().getSimpleName()); } } publicclassCreatorGeneric{ publicstaticvoidmain(String[]args){ Creatorc=newCreator(); c.f(); } }
具体类型的创建放到了子类继承父类时,在create方法中创建实际的类型并返回。
总结
本文介绍了Java泛型的使用,以及类型擦除相关的问题。一般情况下泛型的使用比较简单,但是某些情况下,尤其是自己编写使用泛型的类或者方法时要注意类型擦除的问题。接下来会介绍数组与泛型的关系以及通配符的使用。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持毛票票!