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

Java的“泛型”特性,你觉得你会吗?

时间:2023-03-15 11:16:13 科技观察

使用Java的小伙伴们一定非常熟悉Java的一些高级特性,比如集合、反射、泛型、注解等,这些可以说是我们日常开发中经常用到的,尤其是集合,基本上只要代码写好了,没有什么不能用的。今天先来说说泛型。1.定义在理解一个事物之前,首先要知道它的定义,所以我们从定义开始,一步步揭开泛型的神秘面纱。#泛型(generics)是JDK5引入的新特性。泛型提供了一种编译时类型安全监控机制,可以让我们在编译时检测到非法类型的数据结构。泛型的本质是参数化类型,即把要操作的数据类型指定为一个参数#普通泛型的意思就是上面的T只是类似于一个形参,名字其实可以任意,但是当我们写代码的时候,要时刻注意可读性。常用参数通常包括:E-Element(用在集合中,因为集合存储元素)T-Type(代表Java类,包括基础类和我们自定义的类)K-Key(代表key,比如key)VinMap-价值(代表价值)?-(代表一个不确定的java类型)但是泛型参数只能是类类型,不能是基本数据类型,而且它的类型必须来自Object注意:泛型不接受基本数据类型,换句话说只能是引用类型作为泛型方法的实际参数。#泛型的引入是java语言的一大功能增强。同时也给编译器带来了一定的增强。为了支持泛型,java类库进行了相应的修改以支持泛型特性。(科普:其实java泛型并不是jdk5提出的(jdk5是2004年发布的),早在1999年,泛型机制就是java最早的规范之一。)另外,泛型还有以下优点:#1.java的类型安全泛型已经提交,很大程度上提高了java的程序安全性。例如,在没有泛型的情况下,很容易将字符串123转换为Integer类型123或Integer转换为String,这样的错误在编译时是检测不到的。使用泛型可以很好的避免这种情况。#2.不需要烦人的强制类型转换泛型之所以能够消除强制类型转换,是因为程序员在开发时已经指定了自己使用的具体类型,这不仅提高了代码的可读性,也增加了代码的健壮性。#提高代码复用性泛型编程是指编写的代码可以被很多不同类型的对象复用在泛型规范正式发布之前,泛型编程是通过继承来实现的,但是像这样存在两个严重的问题:①需要进行类型转换取值时,否则将获取所有对象。②编译时不会有错误检查。我们来看看这两个错误的产生。2.1编译时不会有错误检查;}}程序不仅会报错,还会正常输出2.2强制类型转换publicclassDonCheckInCompile{publicstaticvoidmain(String[]args){Listlist=newArrayList();list.add("a");list.add(3);for(Objecto:list){System.out.println((String)o);}}}因为不知道实际集合中的元素是什么类型,所以在使用的时候也不确定。如果强加于人,难免会带来意想不到的惊喜错了,这样的潜在问题就像一颗定时炸弹,是绝对不允许发生的。所以这更加体现了泛型的重要性。3.泛型方法在java中,泛型方法可以用在成员方法、构造方法和静态方法中。语法如下:publictypeparameterfun();比如publicTfun(Tt);其中T代表泛型,意思是我们定义了一个类型T的类型,这样的T类型可以直接使用,需要放在方法的返回值类型之前。T表示在声明的时候不知道具体的类型,只有在使用的时候才能明确它的类型。T不是类,但可以用作类型。下面通过具体的例子来说明。下面的代码交换数组中两个指定下标位置的元素(不用注意实际要求是什么)。第一类Integer类型数组publicclassWildcardCharacter{publicstaticvoidmain(String[]args){Integer[]arrInt={1,2,3,4,5,6,7,8,9};change(arrInt,0,8);System.out.println("arr="+Arrays.asList(arrInt));}/***交换数组中指定两个下标位置的元素**@paramarrarray*@paramfirstIndexfirstsubscript*@paramsecondIndexsecondsubscript*/privatestaticvoidchange(Integer[]arr,intfirstIndex,intsecondIndex){inttmp=arr[firstIndex];arr[firstIndex]=arr[secondIndex];arr[secondIndex]=tmp;}}第二种是String类型的数组不会直接编译通过,那是必然的,因为方法定义的参数是Integer[],所以你传一个String[],玩玩吧。..所以这个时候,我们只能再定义一个String[]的参数类型。如果还有另一个Double怎么办?布尔值呢?这是个问题吗?虽然这种问题并不致命,可以通过多写一些重复的代码来解决,但不可避免地会导致代码冗余,增加维护成本。.所以这个时候泛型的作用就体现出来了,我们就改成泛型的方式。/***@paramt参数类型T*@paramfirstIndex第一个下标*@paramsecondIndex第二个下标*@param表示定义了一个类型T,否则没人知道T是什么,编译不知道期间*/privatestaticvoidchangeT(T[]t,intfirstIndex,intsecondIndex){Ttmp=t[firstIndex];t[firstIndex]=t[secondIndex];t[secondIndex]=tmp;}接下来调用就简单了publicstaticvoidmain(String[]args){//首先定义一个Integer类型的数组Integer[]arrInt={1,2,3,4,5,6,7,8,9};//设置第一个ChangeT(arrInt,0,8);System.out.println("arrInt="+Arrays.asList(arrInt));//然后定义一个String类型的数组String[]arrStr={"a","b","c","d","e","f","g"};//交换第一和第二位置的元素changeT(arrStr,0,1);System.out.println("arrStr="+Arrays.asList(arrStr));}问题很容易解决。至于普通泛型方法和静态泛型方法,用法是一样的,只不过是数据类和属于类的实例没有太大区别(但需要注意的是,如果静态泛型方法在泛型类不能使用泛型类中的泛型类型,这个在下面泛型Types中会详细说明)。最后我们看一下构造方法publicclassFather{publicFather(Tt){}},假设他有一个像这样的子类classSonextendsFather{publicSon(Tt){super(t);}}这里强调一下那是因为Father类中没有无参构造函数,取而代之的是有参构造函数,但是这个构造方法是泛型方法,那么这样的子类就必须要显式指定构造器。通过泛型方法获取集合中的元素test既然说泛型在声明的时候不是重点,那么只要在使用的时候确认一下就好了,看看下面是怎么解释的?这时候如果想往集合中添加元素,却提示这样的错误,连编译都无法通过。为什么?因为此时集合List的add方法添加了一个T的类型,但是显然T是一个泛型类型,真正的类型只有在使用的时候才能确定,而在add中无法确定T的类型。所以根本不能使用add方法,除非list.add(null),但这没有任何意义。4.泛型类首先看这样一段代码,它使用了多个泛型方法,所以不需要关注方法是干什么的。publicclassGenericClassTest{publicstaticvoidmain(String[]args){//先定义一个Integer类型的数组Integer[]arrInt={1,2,3,4,5,6,7,8,9};//交换处的元素第1和第9个位置newGenericClassTest().changeT(arrInt,0,8);System.out.println("arrInt="+Arrays.asList(arrInt));Listlist=Arrays.asList("a","b");testIter(list);}/***@paramt参数类型T*@paramfirstIndex第一个下标*@paramsecondIndex第二个下标*@param表示定义了类型T的一个类型,否则没有知道T是什么,编译期就不知道*/privatevoidchangeT(T[]t,intfirstIndex,intsecondIndex){Ttmp=t[firstIndex];t[firstIndex]=t[secondIndex];t[secondIndex]=tmp;}/***遍历集合**@paramlistcollection*@param表示定义了一个类型T,否则没人知道T是什么,编译时也不知道*/privatestaticvoidtestIter(Listlist){for(Tt:list){System.out.println("t="+t);}}}可以看到里面的每个方法是否都needs被声明一次。如果有100个方法呢?那有必要吗?它被声明100次,然后将应用泛型类。泛型类的形式是什么样的?请看代码publicclassGenericClazz{//Thisiswhatisthemostbasicgenericclasslookslike}下面我们将刚才的代码优化如下,不过这里不得不说一个很Basic,但是很少有人注意到这个问题,请看下面截图中的文字说明。#为什么实例方法没问题,静态方法却报错?1.先告诉你结论:静态方法不能使用类定义的泛型,应该单独定义泛型。2、估计很多朋友瞬间就明白了,因为静态方法是通过类直接调用的,而普通方法必须通过实例来调用。当一个类调用静态方法时,后面的泛型类还没有创建,所以一定不能这样调用。所以在这个泛型类中的静态方法可以直接这样写/***遍历集合**@paramlist集合*/privatestaticvoidtestIter(Listlist){for(Kt:list){System.out.println("t="+t);}}多个泛型同时使用。我们知道Map是以键值对的形式存在的,那么如果我们对Map的Key和Value都使用泛型怎么办呢?同样的用法,一个静态方法就可以搞定,请看下面的代码entrySet()){Kkey=kvEntry.getKey();Vvalue=kvEntry.getValue();System.out.println(key+":"+value);}}publicstaticvoidmain(String[]args){Mapma??pStr=newHashMap<>();mapStr.put("a","aa");mapStr.put("b","bb");mapStr.put("c","cc");mapIter(mapStr);System.out.println("======");Mapma??pInteger=newHashMap<>();mapInteger.put(1,"11");mapInteger.put(2,"22");mapInteger.put(3,"33");mapIter(mapInteger);}}至此,5的泛型方法和泛型类就介绍完了。Wildcard?是占位符的意思,就是在使用的时候不能确定它的类型,只要在以后实际使用的时候指定类型即可,它有三种形式无限通配符。就是为了让泛型可以接受未知类型的数据带上限的通配符。可以接受指定类及其子类类型的数据,E是泛型类型的上限带下限的通配符。可以接受指定类及其父类型的数据,E是泛型的下边界5.1刚刚提到的通配符,用一个类型来表示自省类型是必须声明的,也就是不声明是不是不能使用泛型声明呢??当然不是。本节介绍的就是解决这个问题。表示,不过话又来了,既然不用指定具体类型,那呢?它不能表示特定类型。也就是说,如果你是按照原来的方式写的,请看代码中的注释,因为任何类型都是Object的子类,所以这里可以用Object来接收。的具体用途?将在以下两节中介绍。另外大家要明白,泛型和通配符不是一回事。5.2通配符表示带上限的通配符,可以接受其类型及其子类类型E指代上限,或者写个例子说明publicclassGenericExtend{publicstaticvoidmain(String[]args){ListlistF=newArrayList<>();ListlistS=newArrayList<>();ListlistD=newArrayList<>();testExtend(listF);testExtend(listS);testExtend(listD);}privatestaticvoidtestExtend(Listlist){}}classFather{}classDaughterextendsFather{}classSonextendsFather{}这个时候,一切还是很平静的,因为大家都遵守约定。无论如何,List中的泛型类型要么是Father类,要么是Father的子类。种类。但是此时如果这样写(具体原因在截图中已经说明)5.3Wildcard表示具有下限的通配符。也就是说它可以接受指定的类型和它的父类型,E是泛型类型的下边界,直接拿出代码解释一下publicclassGenericSuper{publicstaticvoidmain(String[]args){ListlistS=newStack<>();ListlistF=newStack<>();ListlistG=newStack<>();testSuper(listS);testSuper(listF);testSuper(listG);}privatestaticvoidtestSuper(Listlist){}}classSonextendsFather{}classFatherextendsGrandFather{}classGrandFather{}因为List列表接受的类型只能是Son或者Son的父类,而Father和GrandFather都是Son的父类,所以没有问题上面的程序,但是如果另一个类是Son的子类(甚至不是与Son相关的类),会发生什么?看下图,图中已经对相关点进行了详细的解释,其实说到泛型这里基本也是这样。我们在开发中经常遇到的问题和不常遇到的问题,在这篇文章中基本都有说明。最后,我们来看看泛型的另一个特性:泛型擦除。6.泛型擦除先看泛型擦除的定义#泛型擦除因为泛型信息只存在于java编译阶段,所以在编译阶段用java泛型编译一个程序后,生成的类文件中泛型相关的信息会被抹掉,从而保证不影响程序运行的效率,也就是说泛型在jvm中和普通类是一样的。别着急,我知道你看完这个概念还是不明白什么是泛型擦除。例如,publicclassGenericWipe{publicstaticvoidmain(String[]args){ListlistStr=newArrayList<>();ListlistInt=newArrayList<>();ListlistDou=newArrayList<>();System.out.println(listStr.getClass());System.out.println(listInt.getClass());System.out.println(listDou.getClass());}}这意味着java泛型没有泛型类型在生成字节码之后根本没有,它们甚至会在编译过程中被擦除。通用擦除非常彻底。下面举个例子一步步证明泛型是通过反射验证在编译时被擦除的。classDemo1{publicstaticvoidmain(String[]args)throwsException{Listlist=newArrayList<>();//这里没有问题,一个普通的集合类添加元素list.add(1024);list.forEach(System.out::println);System.out.println("-------通过反思证明泛型在编译过程中被擦除------");//如果不懂反射,别着急,想看launch的文章欢迎留言反射,我保证下期完成list.getClass().getMethod("add",Object.class).invoke(list,"9527");for(inti=0;i是泛型的!连最基础的知识都不要忘记fanShe.getClass().getMethod("setStr",Object.class).invoke(list,"2222");System.out.println(fanShe.getStr());}}//写一个类classFanShe{privateIntegerstr;publicvoidsetStr(Integerstr){this.str=str;}publicIntegergetStr(){returnstr;}}测试结果很明显,非泛型不能通过反射修改类型赋值泛型擦除导致的自动类型转换由于泛型类型擦除问题,所有泛型类型变量在编译后都会被替换为原始类型。既然都是替换成原来的类型,为什么我们拿到的时候不用cast呢?以下是具有通用返回值的标准方法。publicclassTypeConvert{publicstaticvoidmain(String[]args){//调用方法时的返回值就是我们实际传递的泛型类型(ClasstClass){//只需要将返回值类型转换为实际泛型类型即可Treturn(T)tClass;}}classMyClazz1{}classMyClazz2{}泛型导致的数组名问题很奇怪吓人,其实说白了就是不能创建泛型数组。看下面的代码。为什么不能创建泛型数组?因为List和List在JVM中被编译为等同于List,所有的类型信息都等同于List,也就是说此时编译器无法区分数组中的具体类型是Integer类型还是String.但是,可以使用通配符。上面我也强调了一句话:泛型和通配符不是一回事。请看代码,这是为什么呢??表示未知类型,它的操作不涉及任何类型相关的东西,所以JVM不会对其进行类型判断,所以可以编译,但是这个方法只能读不能写,也就是只能可以用get方法,不能用add方法。为什么不加?提供只读功能,即删除了添加特定类型元素的能力,只保留与特定类型无关的功能。它不关心这个容器中加载的是什么类型的元素,它只关心元素的数量和容器是否为空。另外上面已经解释了为什么不能加,这里补充一下。好了,今天的科普知识就到这里了,感谢大家的支持!本文转载自微信公众号“程序员小灰”,可通过以下二维码关注。转载本文请联系程序员小灰公众号。