只要了解了泛型的一般使用情况就能够解决多半的问题。所以,首先我们来了解一下什么是泛型、为什么要使用它以及应用方法。
什么是泛型?
试想一个简单的添加方法(method),如下:
long,float 或 double 类型并不能当作输入传给这个方法。
如果从该方法中抽象出数据类型,就可以得到一个新的方式,如下。
在这里,是 泛型参数(也称为类型变量),和给某一方法声明的参数一样。给 或 传递的泛型参数的值,与方法参数相似,叫做类型参数。
现在考虑数据结构,简单起见,我们来想一想数组。我们能够创建一个任意类型的数组吗?不可以。我们只能创建一个整数数组、浮点数数组或者其他一种特定类型的数组。好了,忘掉所有编程语言里实现数组的方法,然后问一个问题:“我们是否可以从这个数据结构中抽象出一种数据类型?”
答案是肯定的。Java 中的 ArrayList 就是做这件事的一种类。通过 List = new ArrayList<>(); 就能创建一个字符串数组,当整数作为类型参数时,它就是整数数组,其他的也类似。
虽然我们用 ArrayList 作为例子,但由于其复杂性,我们不会讨论他们具体是怎么实现的。我们会借鉴一个盒子并思考怎么把这个盒子做出来,而这个盒子就是某个特定类型的通用框架(a Generic box from a Specific Typed box)。
思考以下代码,将一个字符串放进特定字符串框架(SpecifizedStringBox)对象中,然后以此获得一个字符串。
现在,如果从该对象中抽取其数据类型“Type”,就得到一个由以下代码代表的通用框架(GenericBox,也就是泛型),而该框架可以使用 String、Integer、Boolean 等任意数据类型。
所以,使用泛型,就是要从某个方法(method)或者类(class)中,抽象出一种适用于任意类型的通用方法/类。
为什么要用泛型?
简单点的答案就是,通过泛型抽象数据类型后,你的代码可以重复使用并且易于维护。
泛型应用在什么地方?
看起来似乎通过重构已有特定类型的方法或框架,就能应用泛型。在处理数据结构和原始数据类型时,似乎还挺容易,但是我们总会在各不相同的类中建立大量的数据类型。泛型编程模式(Generic Programming Paradigm)和 面向对象编程(OOP)混合在一起时,就很难决定是否要使用泛型。理解在哪里应用泛型,问题就解决了一半。
本文就将带你了解一些典型的泛型用例,包括其使用场景,也可以让你在遇到同类型问题时能够合理应用泛型。
Java 在 JDK 5.0 中引用泛型的目的在于:
类型安全性(Type safety):一旦使用类型参数后,在该方法或框架中就不存在其他的数据类型,同时也避免了类型转化的需求;
通用编程及参数的多态性。
C++ 的模版编程能帮我们实现通用编程及参数的多态性,根据数据的类型(预定义或用户定义的)转化同样的算法模型,达到复用同一个代码或程序的目的。在 Java 中也可以使用类似的方法。
现在来看一下几个常用的泛型用例。
用例 1 : 泛型的第一级别用法是算法和数据类型
算法和数据结构并行,数据类型的微小变化可能会改变一个算法的复杂性。
数据结构中的数据有类型,用泛型将这种类型抽取出来,可以作为类型参数。而算法的输入参数也具有数据类型,同样,通过泛型可以将该类型从输入参数中抽象出来。因此,泛型适用于使用特定数据结构的任意一种算法。
不过事实上,泛型主要用于 Java 的集合 API。
如果你自己写数据结构,那么一定试试利用泛型。除了Java 的集合 API,你也会在 Guava、Apache Common Collection、FastUtils、JCtools 和 Eclipse Collection 里发现其他对泛型更好的应用。
用例 2 :数值输入框或者单个元素的容器
具备可通用类型的数据结构,可以称之为泛型框架(Generics Boxes)。例如 ArrayList、LinkedList 等等这样的类就代表数据类型,同时为他们同类型的数据起着泛型框架的作用。
有时候,通用框架以单个元素而不是集合的形式出现。诸如 Map 映射中的输入 ,节点 ,数据对 ,以及其他代数数据类型,像是可选项 , 选择 等等,它们只作为特定类型数据的依托(Holder)或封装器(wrapper)而已。
ThreadLocal 和 AtomicReference 在适用于并发访问算法的单元素容器中,是非常好的例子。
类似的用法有时合理,而有一些则不太适
用。 一个盒子在早期确实可以容纳任何类型的物品,但现在会将其进行分类:这个盒子用来装玩具,而下一个盒子用来装笔,等等。
杯子是很好的例子,可以把它比做实时对象类型的依托物(Holder),它可以装茶、咖啡或者任何饮料。公交上可以坐男人和女人,如果让公交具备类型安全性且只允许女人上车,那么我们可以称之为女士公交。这种比喻可能有点欠妥,但它提出了商业用例,尤其是封装器或者依托物也具有应用泛型的可能。尝试询问业务的封装或依托是否有使用数据结构的倾向,如果有,那么使用泛型会更好。
用例类型 3 :抽象类型的泛型工具方法
泛型算法不一定总是和特定的数据结构或算法绑定在一起。有时,基于实际应用的满意度,它还可以应用在大多数抽象数据结构组中。
在 Java 中就有该Collections工具类。
查看以下方法,了解什么方法能适用:
Collection Factories Methods, Empty/Singleton
emptyList, emptyMap, emptySet
singleton, singletonList, singletonMap
封装方法(Synchronized, UnModifiable, Checked Collection):
synchronizedCollection, synchronizedSet, synchronizedMap
unmodifiableCollection, unmodifiableSet, unmodifiableList
checkedCollection, checkedList, checkedSet
还有一些泛型方法,可归为四大类:
1. 更改列表中的元素顺序:reverse,rotate,shuffle,sort,swap;
2. 更改列表内容:copy, fill, replaceAll;
3. 在集合中寻找极值:最大值,最小值;
4. 在列表中查找特定值:binarySearch,indexOfSubList,lastIndexOfSubList。
由于他们适用于列表中的任意类型,这些都是可复用的功能。我们会发现,大多数集合都适用泛型方法。
用例类型 4 :泛型方法用于类的分层并行结构中
Spring 框架中的 JpaRepository、CrudRepository 都已使用泛型构建,创建、更新、查找、查找所有、删除等等,是适用于所有实体的泛型方法。
需要给每个实体创建一个并行数据访问对象(DAO)类时,会出现类的分层并行结构(parallel hierarchy of classes)。 不过 DAO 模式并不是其出现的唯一情况。
如果为了提供更多可能的方法实例,我们可以通过将方法与对象解除联系的方式,来应用策略模式(Strategy Pattern)处理业务问题,这时类的分层并行结构就会出现。
每当我们添加一个新类,就会增加一个并行的测试用例。如果需要工厂,我们就添加一个并行工厂类。 类的分层并行结构出现在业务用例中。试想一辆新车,比如“大巴车”,把它添加到以下的车辆层级中时,可能还需要添加一个“大巴车司机”的类。
来看以下分层并行类和其泛型的例子:
用例类型 5 : 创建类型安全的异构容器
集合 是均质容器的一个示例,任何字符串以外的东西都不能放进该框架里。
而集合 是异构容器的一个例子,可以放入任意对象。集合 便不是类型安全的,需要检查类型、进行转换,类似于原始类型的集合(原始类型是没有泛型类型参数的通用类型,它将对象视为默认的类型参数)。Java 没有为类型安全的异构容器提供第一级别的支持。
在集合 中,类型参数“String”被用作类型参数“T”,以确保类型安全。但是对于 Map,它却有两个类型参数,通过之前Java 集合 API 的例子,通常泛型会限制每个容器类型参数的数量为一个定值。可以将类型参数设置在Map映射的键(key)上,而不是容器上,从而绕过这个限制。在建立类型安全的异构容器/Map映射时,利用类对象作为键。
像是 bean 容器,异常处理程序容器,或服务查找容器都是异构容器的示例,都可以使用泛型来进行类型安全化,方法既使用类对象作为键实现动态转换。