泛型是Java的高级特性之一。如果你想写出优雅且可扩展性强的代码,或者想了解一些优秀的源代码,泛型是一个不可避免的障碍。本文介绍了什么是泛型,类型擦除的概念及其实现,最后总结了使用泛型的最佳实践。前言我想写一篇关于Java的一些高级特性的文章。虽然这些特性在实现普通业务时并不需要用到,但是如果你想写出优雅且扩展性强的代码,或者想阅读和理解一些优秀的源代码,这些特性又是不可避免的。如果不了解这些特性,不熟悉特性的应用场景,并且由于语法等原因使用起来困难重重,人们很难克服惯性去使用,所以总有我周围的一些同事工作多年但从未使用过它们。通过Java的一些高级特性,写出来的代码总感觉差了点。为了避免几年后自己的代码还是很low,我打算从现在开始对这些特性有更深入的了解。本文先写一下应用场景最多的泛型。什么是泛型首先,什么是泛型。generic英文是generic,中文是通用的意思。结合它的应用场景,我理解泛型就是通用类型。但是我们一般把泛型说成是它的实现,也就是参数化类型。对于Java这样的强类型语言,如果没有泛型,处理同一个逻辑不同类型的需求会很麻烦。如果要写一个int数据的快速排序,我们编码为(不是主角,网上随便找的=_=):publicstaticvoidquickSort(int[]data,intstart,intend){intkey=data[start];inti=开始;intj=结束;while(ikey&&j>i){j--;}data[i]=data[j];while(data[i]start){quickSort(data,start,i-1);}if(i+1>voidquickSort(T[]data,intstart,intend){Tkey=data[start];inti=start;intj=end;while(i0&&j>i){j--;}data[i]=data[j];while(data[i].compareTo(key)<0&&istart){quickSort(data,start,i-1);}if(i+1的形式,当需要在一个地方同时声明多个占位符时,使用,分隔。占位符的格式没有限制,但一般约定使用单个大写字母,比如T代表类型(type),E代表element*(元素)等。虽然没有严格的规定,对于代码的易读性,最好在使用前检查约定用法。泛型是指可以在类、方法和接口上声明的参数类型。我们最常在类上声明泛型类型:classGenerics{//declare在类名之后引入泛型类型privateTfield;//引入之后,字段就可以声明为泛型类型publicTgetField(){//在类方法中也可以使用泛型returnfield;}}在方法上声明泛型:public[static]voidtestMethod(Targ){//accessqualifier[staticmethodisdeclaredafterstatic]with泛型方法之后,在参数列表之后可以使用泛型//doSomething}最后在接口中声明泛型,如上面的快速排序,我们使用了Comparable的泛型接口,而这个类似于SearializableIterable等。其实在接口中声明和在类中声明并没有太大区别。调用之后是通用调用。泛型调用和普通方法或类调用没有太大区别,如下所示:publicstaticvoidmain(String[]args){String[]strArr=newString[2];//泛型方法调用与normalmethodGenerics.quickSort(strArr,0,30);//泛型类调用时需要声明精确类型Genericssample=newGenerics<>();Longfield=sample.getField();}//Generic接口需要在泛型类中实现classGenericsImplimplementsComparable{@OverridepublicintcompareTo(To){return0;}}类型擦除只有擦除才能理解泛型,才能避免使用泛型时的陷阱。严格来说,Java的泛型并不是真正的泛型。Java的泛型是JDK1.5之后增加的特性。为了兼容之前版本的代码,其实现引入了类型擦除的概念。类型擦除指的是:Java的泛型代码在编译的时候会经过编译器的类型检查,然后它的泛型类型就会被擦除,只保留原来的类型。比如Generics被擦除后就是Generics,我们常用的List被擦除只剩下List。以下Java代码在运行时仍然使用原生类型,并没有称为泛型的新类型。这样也能兼容泛型之前的代码。比如下面的代码:()){System.out.println(stringList.getClass().toString());System.out.println(longList.getClass().toString());System.out.println("typeerased");}}ResultlongList和stringList的输出类型都是classjava.util.ArrayList,并且两个类型相同,说明它们的泛型已经被抹掉了。其实在泛型代码的字节码中会有一个signature字段,指向常量表中泛型的真实类型,所以也可以通过反射获取泛型的真实类型。实现了类型擦除后,Java如何保证泛型代码在执行过程中不出问题呢?我们用javac命令将一段泛型代码编译成class文件,然后用javap命令查看其字节码信息:我们会发现类型中的T被Object类型替换了,而当getField字段中的main方法,进行了类型转换(checkcast),所以我们可以看到已经实现了Java的泛型类型。一段泛型代码的编译运行过程如下:在编译过程中,编译器检查传入的泛型类型是否与声明的泛型类型匹配,如果不匹配,则报编译错误;编译器进行类型擦除,字节码中只保留原始类型;在运行时,对象被转换为所需的通用类型。也就是说:Java的泛型实际上是由编译器实现的,编译器将泛型类型转换为Object类型,然后在运行时进行状态转换。实际问题从上面我们来看使用泛型需要注意的问题:具体类型必须是Object的子类型。上面说了,实现泛型时声明的具体类型必须是Object的子类型,因为编译器进行类型擦除后,泛型会被替换为Object,运行时会进行类型转换,而基类型和Object无法更换或转换。例如:Genericsgenerics=newGenerics();编译时会报错。边界限制通配符的使用泛型虽然是泛型类型,但是其通用性也是可以设置的,所以才有了边界限制通配符,而边界限制通配符只能用类型擦除来理解。是一个受上限限制的通配符。我们看语句xxextendsGenerics,XX是一个继承Generics的类(也可能是一个实现,下面只讲继承),我们按照如下代码声明:ListgenericsList=newArrayList<>();Genericsgenerics=genericsList.get(0);genericsList.add(newGenerics());//编译失败,我们会发现最后一行编译出错。至于为什么,我们可以这样理解:XX是一个继承Generics的类,从List中取出来的类必须转为Generics,所以get方法就可以了;我们不知道具体的类是什么,我们将父类强制转换为子类可能会导致运行时错误,所以编译器不允许这样做;同样,是下界通配符,XX是Generics的父类,所以:ListgenericsList=newArrayList<>();genericsList.add(newGenerics());//编译不能通过Genericsgenerics=genericsList.get(0);在使用之前,需要根据这两种情况考虑get或者set,然后决定使用whichboundsqualifythewildcard。最佳实践当然,泛型并不是一个放之四海而皆准的容器。将任何类型扔到泛型中都比直接使用Object类型要好。什么时候使用泛型以及如何使用泛型?这些问题的解决不仅仅依赖于编程经验。下面我们就用开头的例子来梳理一下泛型的实践:1.将代码逻辑分为两部分:通用逻辑和类型相关逻辑;通用逻辑是一些与参数类型无关的逻辑,比如快速排序中对元素位置进行排序;type-relatedlogic,顾名思义,就是写之前需要确定的逻辑,比如元素大小的比较,String类型的比较,int类型的比较不一样。2、如果没有类型相关的逻辑,比如List作为容器不需要考虑任何类型,那么直接改进通用代码即可。3、如果存在与参数类型相关的逻辑,那么就要考虑这些逻辑是否有共同的接口实现。如果有通用的接口实现,可以使用边界限制通配符。比如快速排序的元素实现了Compare接口,Object已经实现了toString()方法,所有打印语句都可以调用。4.如果没有通用的接口,那么就需要考虑能不能抽象出一个通用的接口实现,比如打印人的衣服颜色,动物的皮毛颜色,可以抽象出一个getColor()接口,然后使用抽象通配符后的边界限制。5.如果不能抽象出通用的接口,比如输出人的身高或者动物的体重,就不要使用泛型,因为如果不限制类型,就无从谈起具体类型的方法调用,以及编译将失败。我把上面的步骤整理成了一个流程图。根据这张图,我们可以很快的找出是否可以使用泛型,以及如何使用泛型。总结好好看了一遍泛型,感觉收获颇丰,Java的迷雾也拨开了。这些特性确实是相当难的。每当我觉得自己理解得差不多了,过了一会儿又觉得自己理解不够。重要的是实践,用起来就会很容易发现疑点。