本系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考,希望能帮助更多(Java)码农和想成为(Java)码农的人。
目录
- 介绍
- 再谈Java中的类型
- 为什么需要泛型?
- Java中的泛型
- 泛型类型
- 泛型方法
- 总结
介绍
还记得我在这篇文章(我的Java Web之路32 - Spring MVC基于注解的控制器)中列举的Handler方法支持的众多返回值类型和注解吗?其中有不少是如下形式的:
- HttpEntity, ResponseEntity
- DeferredResult
- Callable
- ListenableFuture, java.util.concurrent.CompletionStage, java.util.concurrent.CompletableFuture
还有这篇文章(我的Java Web之路52 - Spring JDBC初步使用)提到 RowMapper 接口是一个泛型接口,使用时是这样的:
new RowMapper() { ... }
还有这篇文章(我的Java Web之路55 - ORM框架(MyBatis)初步使用)中我们定义HouseMapper接口时,其中一个方法的返回值使用了泛型接口List:
List selectAll();
还有这篇文章(我的Java Web之路58 - Spring整合ORM(MyBatis)2)也提到MapperFactoryBean实际上是一个泛型类,使用基于Java配置的方式如下:
@Beanpublic MapperFactoryBean houseMapper() throws Exception { ... }
这些类和接口使用时都在类名或接口名后面添加了一对尖括号括起其他类名的内容,这就是Java中的泛型,本篇文章就粗略介绍一下,让我们对它有个基础的认识,以后遇到它就能够理解了。
再谈Java中的类型
大家都知道,Java语言虽然是一种面向对象的编程语言,但同时也是一种强类型的编程语言,即任何一个变量都需要先声明它的类型之后才能使用它。关于类型和变量的一些知识,大家可以参考这篇文章和这篇文章。
总的来说,Java中的类型有两种,一种是基本类型(英文是primitive types),包括八个:byte、short、int、long、float、double、char、boolean 。
另一种是引用类型(英文是reference),因为引用类型是指向某一个对象/实例的,所以如果某个变量是引用类型的话,通常我们说该变量是某某类的引用,即该引用所指向的对象/实例的类型是某某类 / 接口。
所以,我们把基本类型和各种类 / 接口都统称为类型,即每一个类 / 接口就是一种类型,也就是说我们可以无限扩展类型,因为我们可以定义无限多个类 / 接口。
为什么需要泛型?
解释了Java中的类型之后,我们再来思考一下为什么需要泛型呢?或者说泛型解决了什么问题呢?
我们经常会遇到这种情况,一些代码逻辑(归为算法也未尝不可,就当是广义上的算法吧)实际上与它处理何种类型的数据是无关的。
举个例子,就拿上面我们经常用到的列表(List)这个类/接口所代表的算法来说,它就好像是现实中的火车一样(可能有些不太恰当),一节一节的,想装旅客就装旅客,想装某种货物就装某种货物,因此就有这样的方法:
E get(int n); //获取第N节车厢的东西,可能是旅客,也可能是其他某种货物E set(int n, E element); //将旅客或者其他某种货物装进第N节车厢
当然,列表(List)这个接口还有很多方法,这里只是拿出这两个来举例。可以看到,列表(List)这种算法(实际上是数据结构)独立于各种数据类型,即它可以容纳各种数据类型的数据。
反过来说,假如没有泛型的话,我们就需要为每一种类型设计一种List,比如:
interface 旅客List { 旅客 get(int n); 旅客 set(int n, 旅客 element);}interface 苹果List { 苹果 get(int n); 苹果 set(int n, 苹果 element);}//必要的话,还需要定义其他List
不知道大家有没有发现上述代码的最大问题是什么?
没错,就是重复啊!除了几个地方的数据类型不同,其他代码都是相同的。所以说,泛型本质上就是解决代码重复问题的。有了泛型,你就可以这样定义List:
interface List { T get(int n); T set(int n, T element);}
使用的时候,我们再指定是何种类型的List:
List list_旅客;List list_苹果;
或许你会想到,既然在Java语言中,一切都是Object类的子类(除了那八个基本类型外),我们可以用Object类来设计List啊:
interface List { Object get(int n); Object set(int n, Object element);}
使用的时候,将类型强制转换不就可以了吗:
List list_旅客;list_旅客.set(0, 旅客A);旅客 旅客A = (旅客)list_旅客.get(0);
不过,这样一来我们就需要使用强制类型转换,而强制类型转换是非常不安全的,比如,往往在某个地方将旅客塞进了某个List,而使用的时候却将它强制转换成苹果,这不就出错了吗。特别是Java编译器是不能发现此类错误的,只有在程序运行时才能发现(错误是越早发现越好)。
而使用泛型,我们在定义某个List变量时就可以指定该List是用来装何种类型的数据的,一来Java编译器可以发现此错误,因为它解析代码的时候就能够记住指定的类型啊;二来我们无需使用强制类型转换。
事实上,Java的泛型在底层就是使用Object来实现的,只不过是由Java编译器为我们进行强制类型转换。
综上所述,泛型有如下好处:
- 消除重复(可以编写独立于类型的算法,即泛型算法);
- 编译时就能进行类型检测;
- 无需使用强制类型转换。
Java中的泛型
Java中的泛型分两种:
- 泛型类型
- 泛型方法
泛型对应的英文单词是 generic :
adj.一般的; 普通的; 通用的; 无厂家商标的; 无商标的;
n.同“a generic drug";
[例句]Parmesan is a generic term used to describe a family of hard Italian cheeses.
帕尔玛干酪是意大利硬奶酪的通称。
[其他]复数:generics
可以看到,泛型这个译法一方面与 generic 的本义(泛)是相符的,另一方面又兼顾了编程领域的含义(类型)。
泛型的本质是将类型变为一种参数,这就叫做类型的参数化(parameterized over types)。我们可以拿它与普通变量的参数化进行类比,这样有助于我们的理解和记忆:
要注意,形参的英文单词是 parameter;实参的英文单词是 argument 。
所以,类型形参就是 type parameter;类型实参就是 type argument 。
泛型类型
泛型类型又包括两种:
- 泛型类
- 泛型接口
泛型类的定义只需要在类名之后添加一对尖括号,然后在尖括号中声明若干个类型形参:
new RowMapper() { ... }
泛型接口的定义也是类似:
List selectAll();
当然,尖括号中声明的类型形参就可以在该类或接口中使用了,不管是在属性中还是在方法中都可以使用,比如:
@Beanpublic MapperFactoryBean houseMapper() throws Exception { ... }
由于历史原因,泛型是在JDK的某个版本引入的,所以JDK中存在将原来不是泛型的类型,在后来的版本中改造成了泛型类型,而为了与之前的版本兼容,所以原来不是泛型的类型仍然可以使用,于是Java就引入原始类型(Raw Type)的概念。
举个例子,ArrayList原来不是泛型,后来被改造成了泛型:
public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable
所以,不使用泛型方式的ArrayList就叫原始类型,我们仍然可以定义原始类型的变量,甚至将泛型类型的对象赋值给该变量,当然,反过来也可以,如下:
ArrayList arrayList = new ArrayList();ArrayList listOfInt = list;
不过,这种代码极其不安全,大家最好尽量避免,一般IDE都会给出提示,Java编译器在编译时也会给出警告。
类型形参的命名也有一些约定俗成的规定,但你也可以不遵从:
E - Element:表示元素的类型,通常用在表示集合、容器等概念的泛型类 / 接口中。K - Key:表示键的类型,通常用在映射概念的泛型类 / 接口中。N - NumberT - Type:一般化的命名。V - Value:表示值的类型,通常用在映射概念的泛型类 / 接口中。S,U,V 等 - 一般化的命名,有多个类型形参时使用。泛型方法
泛型方法是指在方法的定义中有自己的类型形参的声明,如果仅仅使用了泛型类型已经声明的形参,那就不算是泛型方法,举一个官方文档中的例子:
public class Util { public static boolean compare(Pair p1, Pair p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); }}public class Pair { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; }}
泛型方法的类型形参的声明是放在方法返回值的前面,也是使用尖括号将若干形参括起来的形式,如上面的 compare() 方法的定义。
实际调用方法时,可以在方法名前写上类型实参,如:
Pair p1 = new Pair<>(1, "apple");Pair p2 = new Pair<>(2, "pear");boolean same = Util.compare(p1, p2);
当然,前辈们已经把Java编译器实现的足够智能,就算你不写类型实参,它也能够从方法实参的类型中推断出来:
Pair p1 = new Pair<>(1, "apple");Pair p2 = new Pair<>(2, "pear");boolean same = Util.compare(p1, p2);
Java编译器的这个功能就叫做类型推断(type inference)。
总结
本篇文章介绍了Java泛型的基本知识,原理上也是很简单的,大家只要把它当做普通类/接口和普通方法即可,只不过是多了一些语法而已。我们只需要记住,泛型主要还是为了
- 消除重复
- 和实现类型安全。
当然,泛型还有很多内容,比如:
- 类型形参可以设定一些边界,比如只允许某个类的子类或父类当做类型实参。
- 泛型类/接口的继承有些不同,并不是说类型实参有父子关系,它们的泛型类就拥有父子关系,事实是它们没有任何关系。
- 泛型中的通配符的使用。
- 类型推断。
- 类型擦除。
- 一些最佳实践。
- 等等。
这些内容以后慢慢介绍。