泛型
§1 尝试从另一个角度理解泛型
泛型的全称其实是泛化类型,对应英文 GenericsType,又因为在java中定义其父类型 ParameterizedType,并在反射时有明显戏份,因此也有人称之为参数化类型。
对于泛型的意义,有一种说法是:用以实现只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException异常。我相信这是相当一部分同学在学习泛型时,授课老师就是这么切入的(泛型可能是在集合类时讲解),但是这种理解方式可以认为非常现象化
当然还有另一种说法:泛型是把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊类型。虽然相对于上面更靠点谱,但严格的说这种说法是片面的。
下面给出一种私以为比较好的对泛型的认知:
- 首先,泛型是一种比较重要的面向对象的程序设计手段。他不仅仅是用来屏蔽区区一个异常的,而是提供了一种在程序设计阶段比较强力的设计工具和抽象的类型符号
- 其次,泛型是设计过程中当前阶段无法确定的某具体类型而使用的抽象符号。泛型作为一种抽象的类型符号存在,他象征了某种具体类型,此具体类型在当前程序设计阶段(比如一个顶层接口)无法确定,此时可以使用一个泛型[符号]代替这个不能确定的实际类型。出现泛型通常意味着在当前的程序设计阶段,需要声明(使用)某类型,但至少在当前无法确认其具体类型。
- 再次,泛型通常意味着具有泛型的成分在当前设计阶段有必要存在,但其对应的实际类型对当前设计阶段并不重要(最经典的例子就是集合类,其元素是个泛型,但整个集合类里这么多方法关系其元素的具体类型了吗,没有),否则应对泛型做限制或使用具体类型
- 又次,泛型在使用时,最终一定会被指派实际类型,否则它在使用中的实际作用约等于Object(这意味着泛型类型的对象的实际类型,无论是什么都是正常的)
- 最后,泛型并不是在设计阶段不能确定具体类型的首选解决方案,首选方案是使用父级类(包括抽象类和接口),当然,如果父类不满足设计要求就要使用泛型(比如父类就是Object)。
§2 声明
§2.1 声明的分类
我们可以理解泛型是按位置区分的,大致可以分为类泛型和方法泛型,具体说泛型可以使用在接口、抽象类、方法参数、方法返回值上,亦在这些位置进行声明。
§2.2 声明方式
泛型是需要声明的,泛型的声明分为隐式声明和显示声明。
隐式声明一般用于直接对类和接口中带有的泛型进行声明,比如:
显示声明一般用于对定义在方法中的泛型进行声明,比如
§2.3 需要注意的问题
在泛型的声明阶段,有两个关键问题需要注意,下面依次说明
§2.3.1 注意泛型声明(形参)和泛型传参(实参)的区别
上文(§2.2) 中的写法都是对泛型的声明。声明的意思是:只知道这里是某种类型,具体啥类型啊,不知道。
可以类比成方法的参数表,如下图,从方法声明上看,fullGetUrl方法有个入参,要传入一个字符串,字符串具体是啥值啊,不知道。
这里强调的声明有两层含义,
第一层,泛型的字面量无论写成什么,它也是一个泛型,不能当实际类型使,不经常使用泛型的同学经常把自己陷在这里。就好像刚刚的例子中,入参params叫什么名字都可以,不会影响方法中参数的使用,但你不能认为这个参数就是某一个特定的值了。再比如:
上面的泛型,只是长得像一个实际类而已,这里的 HttpServletRequest 和咱平时用的请求本求没有半点关系,这里写个HttpServletRequest ,写个String,写个P本质上都是一样的,红框处甚至都没有 HttpServletRequest 的导包(但这点不能作为判断依据)。
这里还有另一种坑人的方式,如下图:
这里看着像是一个泛型声明,但其实这里是一个泛型的实参,注意看中间那行的导包。这是因为子类继承父类后没有声明泛型,则Ide会认为父类的泛型已经被指定了,父类泛型中写了个T,而正好巧了,Ide正好找到了一个T,于是自动导入了(这个莫名其妙T类坑过很多人)。
第二层,强调既然有声明就一定有赋值。既然在某一个设计阶段,某类型不能确定进而使用了泛型,那就意味着在后续的设计阶段(甚至可能延伸至运行阶段),这个泛型最终是可以确定的。若定义了一个泛型,但在使用时没有指定实际类型,则相当于指定实际类型为Object,显然这是没有意义的。
§2.3.2 使用良好的泛型声明
承接上文:虽然泛型的字面量无论写成什么它也是一个泛型,但也不能得什么写什么。这里的要求是考虑代码的可读性,通常有两种容易挖坑的使用场景:
第一种,实在型,处于阅读性的考虑,泛型的名字体现其含义,但导致它长得太像一个类了(因为名字体现含义是类的命名思路)。比如刚刚出现的例子,如下图,如果把泛型定义的如此望文生义,很容易误导阅读者认为这就是一个实际类型,尤其是泛型的名字就是一个已存在的类型的名字
另一种,报数型,处于区分泛型和实际类型的考虑,使用非常简短的标记,但是,如同报数一样使用类似A,B,C的方式定义。比如下图
这在日常使用中一般不会造成什么困扰,但是,当泛型进行传递/继承的过程中,或者别人对你开发的泛型类进行继承和扩展时,报数型的泛型可以起到良好的“让你懵逼”的效果(鬼知道这俩参数谁对谁,啥意思)
这里给出推荐的泛型命名方式:
- 使用单字母,最多双字母定义泛型
- 单字母时没有特别的具有意义的场景下使用字母T
- 泛型名尽量使用又代表性的单词的首字母,后面给出对应参照表
- 出现泛型的场景,尽量在接口(不仅仅是interface),并且尽量给足注释
字母 | 单词 | 含义 | 场景备注 | |
1 | T | Type | 类型 | 通用 |
2 | T | Target | 目标、靶 | 表示备操作方、目的等 |
3 | E | Element | 元素 | 用于单泛型的集合或带有聚集性质的对象中 |
4 | E | Enum | 枚举 | 并不是java的Enum,而是此类型的值(实例)可以一一罗列的类型 |
5 | D | Data | 数据 | 某种含义下和 T 一样通用 |
6 | D | Desc/Destination | 目的、靶 | 通常和 S 配对使用 |
7 | S | Src/Source | 源 | 通常和 D 配对使用 |
8 | K |
key |
键 | |
9 | V | Value | 值 | 可以单独使用,意为值,或和 K 配合使用意为键值 |
10 | C | Condition/Config | 条件、配置、参数、要素 | |
11 | N | Node | 节点 | 通常用于底层具有链或树的场景下 |
12 | R | Result | 结果 | |
13 | -- | <any> | 根据场景灵活定义 | 比如分页工具的泛型可以使用<P> |
§3 赋值/传参/实参化
§3.1 概念
泛型的传参,即给泛型赋予一个具体的类型,也称泛型的赋值、指定等。
§3.2 赋值的原则和时机
有一种说法是,泛型在使用时进行赋值。但这种说法并不准确,因为本质上,泛型是一种设计手段,因此其赋值原则为:在程序设计时,若当前阶段可以确认泛型位置的具体类型,就应该给泛型进行赋值
在上面的原则上,可以确定泛型传参的上下限制,具体描述如下,并给出对应的例子
泛型最早在可以明确具体类型时赋值,这经常在子类继承带有泛型的父类时发生,父类的类泛型被赋值
泛型最晚在泛型类实例在被使用的过程中赋值,这经常发生在使用泛型类对象方法时,泛型类的方法泛型被指定
继承时的类泛型赋值
使用时方法泛型赋值
§3.3 区分实参形参的方法
最硬(fei)核(hua)的方式:理解泛型实参形参的具体区别,并可以熟练使用,那么一眼就能看出来了
最直观的方式(推荐):对IDE进行配色,实际类型和泛型使用不同的颜色则一眼就能看出来,比如 §3.2 中的图例
最偷(wu)懒(nao)的方式:遇到分不清到底是泛型还是具体类型的场景,按着 CTRL 用鼠标捅一下,能进入具体类型/接口的就是实参
§4 继承/传递
泛型的继承发生在持续设计中,比如,接口中定义了泛型,接口的抽象实现中不能把泛型扔了,并且这个阶段也不能明确泛型的实际类型,因此只能继承泛型
泛型的继承/传递过程中可能发生如下三种情况(注意这里说的是泛型的继承不是泛型类的继承):
- 完整的传递,这一级设计可能是实现了当前类的一些功能,和泛型没关系。例如:AbstractList完整的继承了List接口的泛型
- 部分指定,在当前阶段,对于父级泛型类的泛型,我们可以明确部分泛型的类型,还有一部分仍不能明确。如接口定义一个数据转换器,将数据从一个类型转换至另一个类型,则两个类型是两个泛型;定义子级接口,转换至字符串的数据转换器,则目的数据类型的字段就可以确认为String,子接口声明上只保留那个未赋值的泛型
- 受限,传递过程中虽然不能明确具体类型,但是可以知道类型的一些情况,并通过泛型的上下限进行约束。比如,接口定一个上下文(Context),上下文中包含一个内部容器content,其类型不确定,可能是集合也可能是某实;在子接口中,明确了此容器应是个集合,但具体是什么结合不确定