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

说说Java泛型的全解吧

时间:2023-03-22 14:32:18 科技观察

本人对Java泛型一直是一知半解,其实用得并不多。直到看完《Effect Java》,看到很多平时不懂的用法,我才下定决心。需要系统学习,记录一下。一、泛型概述:1.1泛型的起源根据《Java编程思想》中的描述,泛型出现的动机:泛型出现的原因有很多,其中最显着的原因之一就是创建容器类.泛型的思想由来已久,比如C++中的模板。模板的精神:参数化类型1.2基本概述泛型的本质是“参数化类型”。说到参数,大家最熟悉的就是定义方法时需要形参,调用方法时需要传递实参。“参数化类型”是将原来的具体类型参数化。泛型的出现避免了强制转换的操作,类型转换由编译器完成,也避免了运行错误。1.3泛型的用途Java泛型也是一种语法糖,在编译阶段完成类型转换工作,避免运行时强制类型转换导致的ClassCastException和类型转换异常。1.4JDK1.5实例化时加入了泛型,大大方便了集合的使用。不要使用泛型:publicstaticvoidmain(String[]args){Listlist=newArrayList();list.add(11);list.add("ssss");for(inti=0;ilist=newArrayList();list.add("hahah");list.add("ssss");for(inti=0;i{}注意:泛型必须是引用类型(非基本数据类型)2.2泛型method泛型方法概述:在方法上定义泛型定义格式:public返回类型方法名(泛型变量名){}注意事项:方法声明中定义的形参只能在本方法中使用方法,而在接口和类声明中定义的类型参数可以在整个接口和类中使用。当调用fun()方法时,编译器会根据传入的实际对象,判断类型参数T所代表的实际类型。classDemo{publicTfun(Tt){//可以接收任何类型的数据返回;//直接返回参数}};publicclassGenericsDemo26{publicstaticvoidmain(Stringargs[]){Demod=newDemo();//实例化DemoObjectStringstr=d.fun("Tom");//传递字符串inti=d.fun(30);//传递number,autoboxSystem.out.println(str);//输出内容System.out.println(i);//输出内容}};2.3泛型接口泛型接口概述:在接口定义格式中定义泛型类型:publicinterface接口名称<泛型类型>{}实例:/***泛型接口定义格式:修饰符接口接口名称<数据类型>{}*/publicinterfaceInter{publicabstractvoidshow(Tt);}/***子类是泛型类*/publicclassInterImplimplementsInter{@Overridepublicvoidshow(Et){System.out.println(t);}}Interinter=newInterImpl();inter.show("hello");2.4源码中泛型的使用下面是List接口和ArrayList类的代码片段。//定义接口时指定一个类型参数,参数名称为EpublicinterfaceListextendsCollection{//在这个接口中,E可以作为一个类型publicEget(intindex){}publicvoidadd(Ee){}}//定义类时,指定一个类型参数。参数的名字是EpublicclassArrayListextendsAbstractListimplementsList{//在这个类中,E可以作为一个类型publicvoidset(Ee){..........}}2.5泛型类从父类派生子类时,不能包含类型参数,需要传递具体类型错误方式:publicclassAextendsContainer{}正确方式:publicclassAextendsContainer{}也可以不指定具体类型,系统会将K、V形参视为Object类型publicclassAextendsContainer{}2.6泛型构造函数构造函数也是一种方法,所以诞生了所谓的泛型构造函数。使用普通方法没有区别,一种是显式指定泛型参数,另一种是隐式推断publicclassPerson{publicPerson(Tt){System.out.println(t);}}使用:publicstaticvoidmain(String[]args){newPerson(22);//隐式newPerson("hello");//显示}特别注意:如果构造函数是泛型构造函数,类也是泛型类如何使用情况下的泛型构造函数:因为泛型构造函数可以显式指定自己的类型参数(需要使用菱形,放在构造函数之前),而且泛型类本身的类型参数也需要指定(菱形是放置在构造函数之后),同时出现两个菱形,会出现一些小问题。具体用法总结在这里。以下示例表示publicclassPerson{publicPerson(Tt){System.out.println(t);}}的正确用法:publicstaticvoidmain(String[]args){Personperson=newPerson("sss");}PS:编译器会提醒你怎么做的参数化类型可能是T或者T的子类。正因为无法确定具体是什么类型,所以add方法是有限制的(null可以添加,因为null代表任何类型),但它可以从列表中获取元素并将它们分配给父类型。如上图第一个例子,第三个add()操作会被限制,因为List和List是List的子类型。表示集合中的所有元素都是Animal类型或其子类List。这就是所谓的上限通配符,是通过关键字extends来实现的。实例化时,指定的类型参数只能是extends之后类型的子类或自身。例如:这样就确定了集合中元素的类型。虽然不确定具体的类型,但至少知道它的父类。然后做点别的。意思是集合中的所有元素都是Animal类型或者它的子类List2.7.3下界通配符下界通配符的意思是参数化类型是T的超类型(包括它自己),层层递进,直到Object被编译编译器没有办法判断get(),所以get()方法是有限制的。但是,可以使用add()方法。add()方法可以添加T类型和T类型的子类型。比如第二个例子,先添加一个Cat类型对象,然后添加两个Cat子类型对象。这种方法是可行的,但是如果你添加一个Animal类型的对象,显然会颠倒继承关系,这是不可行的。表示集合中的所有元素都是Cat类型或其父类List。这就是所谓的下限通配符,是通过关键字super来实现的。实例化时,指定的类型参数只能是extends后类型的子类或自身例如//Animal是它的父类Listlist=newArrayList();2.7.4Unboundedwildcard任意类型,不明确则为Object,任意Java类。无限通配符由?任何类型都可以表示任何类型,只有null可以表示任何类型(Object本身也是一种类型,但是不能表示任何类型,所以List和List的含义是不一样的。前者的类型是Object,也就是继承树的顶层,后者的类型完全未知)3.泛型擦除3.1概念编译器在编译带有类型描述的集合时会去除类型信息3.2验证示例:publicclassGenericTest{publicstaticvoidmain(String[]args){newGenericTest().testType();}publicvoidtestType(){ArrayListcollection1=newArrayList();ArrayListcollection2=newArrayList();System.out.println(collection1.getClass()==collection2.getClass());//两者的class类型相同,即字节码一致System.out.println(collection2.getClass().getName());//classes都是java.util.ArrayList,没有实际的类型参数信息}}输出结果:truejava.util.ArrayList分析:这是因为对于泛型类型参数无论传入哪个类型参数,在Java处理时它们仍然被视为同一个类,在内存中只占用一个内存空间。从Java泛型概念的目的来看,它只作用于代码编译阶段。在编译过程中,泛型的结果检查正确后,泛型的相关信息将被抹去,即编译成功后的类文件不包含任何泛型信息。通用信息不进入运行时阶段。静态方法、静态初始化程序块或静态变量的声明和初始化中不允许使用类型参数。由于系统中并没有实际生成泛型类,因此在instanceof操作符之后不能使用泛型类参数示例:publicclassGenericTest{publicstaticvoidmain(String[]args)throwsException{getParamType();}/*使用反射获取实参typeofthemethodparameter*/publicstaticvoidgetParamType()throwsNoSuchMethodException{Methodmethod=GenericTest.class.getMethod("applyMap",Map.class);//获取方法的泛型参数类型Type[]types=method.getGenericParameterTypes();System.out.println(types[0]);//参数化类型ParameterizedTypepType=(ParameterizedType)types[0];//原始类型System.out.println(pType.getRawType());//实际类型参数System.out.println(pType.getActualTypeArguments()[0]);System.out.println(pType.getActualTypeArguments()[1]);}/*参数类型测试方法*/publicstaticvoidapplyMap(Mapma??p){}}输出结果:java.util.Mapinterfacejava.util.Mapclassjava.lang.Integerclassjava.lang.String通过反射绕过编译器对泛型的类型限制publicstaticvoidmain(String[]args)throwsException{//定义一个包含intArrayList();al.add(1);al.add(2);//获取链表的add方法,注意这里是Object.class,如果写int.class,NoSuchMethodException会抛出Methodm=al.getClass().getMethod("add",Object.class);//在反射中调用add方法添加一个string类型的元素,因为add方法的实参是Objectm.invoke(al,"hello");System.out.println(al.get(2));}5泛型限制5.1歧义错误ForgenericclassUserpublicclassUser{publicvoidshow(Kk){//错误信息:'show(K)'clasheswith'show(V)';bothmethodshavesameerasure}publicvoidshow(Vt){}}由于泛型擦除,两者本质上是同一类型的Object,方法相同,所以编译器会报错错误。另一种方式:publicclassUser{publicvoidshow(Stringk){}publicvoidshow(Vt){}}使用结果:可以正常使用5.2类型参数无法实例化编译器不知道创建什么类型的对象publicclassUser{privateKkey=newK();//错误:类型参数'K'不能直接实例化}5.3对静态成员的限制静态方法不能访问定义在类上的泛型;如果静态方法操作的类型不确定,则必须在方法上定义泛型。如果静态方法要使用泛型,则静态方法必须定义为泛型方法。publicclassUser{//ErrorprivatestaticTt;//ErrorpublicstaticTgetT(){returnt;}//更正publicstaticvoidtest(Kk){}}5.4泛型数组的限制如果元素类型是类型参数则不能实例化数组,但是可以将数组指向兼容类型数组的引用publicclassUser{privateT[]values;publicUser(T[]values){//报错,无法实例化元素类型为类型参数的数组this.values=newT[5];//正确,可以将values指向类型兼容的数组引用this.values=values;}}5.5泛型异常的限制泛型类不能扩展Throwable,也就是说泛型异常无法创建类