重要声明:本文章仅仅代表了作者个人对此观点的理解和表述。读者请查阅时持自己的意见进行讨论。
一、泛型简介
泛型
听名知意,它是和Java中数据类型有点关系的东西,它本身并不是某个类型。为了更好的使用它,必须弄明白它具体的设计理念。不妨做一个生活中的比喻来解释这样的设计理念:
-
果盘
我们家里大厅茶几上基本上都会有一个果盘,每当来客人的时候,我们就会将各种水果放在果盘里。因为它是果盘,我们将水果放在里面这很正常,我们总不会把臭袜子放在里面,对吧! -
钱包
大多数情况下,我们钱包都是拿来装钱的,只要是个人,一看到钱包,就肯定想到它里面应该有钱。总不会把家庭作业塞钱包里嘛。 -
茶杯
喝茶是常用的就是茶杯,茶杯里装的是茶水。虽然你可以把今晚炖的龙骨汤倒进去喝,但谁没事儿闲着会干这样的事情嘛,对吧!
可见泛型
在生活中处处可见,它的出现就是为了让我们知道某一个东西是用来处理某类事物的,这就是泛型
。我们在编写程序的时候常常出现一个工具方法返回了一个对象,但我们设计之初的时候此方法可能返回多种不同类型,而在泛型
出现之前,我们就必须小心又小心去处理这些不同的类型。现在有了泛型
,在我们编写代码的时候就完成了对返回类型直观说明,这就极大的避免了许多可能出现的隐藏bug。官方说法就是:Generics add stability to your code by making more of your bugs detectable at compile time. 翻译成中文(百度翻译)就是:泛型通过在编译时检测更多的错误来增加代码的稳定性。
二、代码说明
果盘,假设我们是用ArrayList
来当果盘,在泛型
出现以前,我们的果盘是这样的:
ArrayList fruits = new ArrayList();
fruits.add("苹果");
fruits.add("桃子");
String fr = (String)fruits.get(0);
很明显,我们虽然有了果盘,但是我们不知道果盘里到底是不是水果,我们只有在拿到一个水果之后试图将其强制认为是一个水果。如果代码穿插很多,代码量大,不知道这个位置拿到的是不是真的水果,这样强制处理难免会在某一个时刻出现错误。
庆幸现在我们有了泛型
,有了泛型就相当于告诉了程序,我们的果盘里只能装水果,别的你不能装,这不就保证了获取的数据就一定是水果了嘛:
ArrayList<String> fruits = new ArrayList<>();
fruits.add("苹果");
fruits.add("桃子");
String fr = fruits.get(0);
通过在定义果盘的时候指定果盘能处理的数据类型<String>
来指定我们的果盘只能容纳String
类型的水果,当我们再次进行获取操作时,甚至都不需要再强制转换,受泛型
限定,我们获取到的一定就是水果,不可能是其他的。
三、自己写支持泛型的类
上面我们提到的ArrayList
是Java内置已经实现好了支持泛型
用法。那么我们自己要写一个能够使用泛型
技术的类又该如何实现呢。
现在不如我们来实现一个钱包,这个钱包你想要它装钱,它就只能装钱你想让它装别的也可以,满满的代码里都是注释,绝对看到高潮:
// Wallet.java
// 普通类定义名称后面,多写一个尖括号,里面写这个类要处理的数据的类型,
// 当然了,这个类型只是一个类型替代符号,那么在这个类里面的所有要用到这个类型
// 的地方,都使用这个符号就可以了。
// 如果有多个,你可以通过英文逗号分隔开。
// 现在我们就用大写字母T表示我们这个钱包支持一个数据类型,这个类型具体是什么类型,
// 在其它地方使用钱包时只需要指定其类型就可以了。
public class Wallet<T> {
// 定义一个数组,放入的钱都在这个数组里面。这里就当我们的钱包只能放10张钱,
// 为什么要定义成Object类型的数组?
// 因为 Object 是所有类型的超类,而且我们这里的T是不能直接通过它进行 new T[10] 这样是错误的。
private Object[] ts = new Object[10];
// 通过put方法允许外界将钱放入钱包。
// 而放入的参数类型,就正好使用我们类上面写的 T,
// 这样一来,钱包被定义成放什么类型,就只能往put方法里
// 传递什么类型的参数。
public void put(T t) {
// 内部实现就简单了,循环找一个空位子把钱放进去。
for (int i = 0; i < ts.length; i++) {
if (ts[i] == null) {
ts[i] = t;
return;
}
}
// 如果运行到这里,肯定钱包被装满了。不管了。
}
// 从钱包里取出一张钱,可以看到方法返回类型被定义为了 T,
// 这样一来就保证了别的地方使用这个方法的时候,返回值就
// 直接使希望的类型,不用再进行强制转换了。
public T get() {
// 内部实现就简单的从数组后面往前面找,发现一张钱就立即返回。
for (int i = ts.length - 1; i >= 0; i--) {
if (ts[i] != null) {
// 这里我们将返回类型强制转换为了我们定义的 T 类型,
// 因为我们数组定义的时候是定义为了 Object 的,
// 而put 方法保证了每次放入的一定是相同的 T 类型,
// 所以我们取出的时候,将其强制转换为 T 类型是不会出问题的。
return (T)ts[i];
}
}
// 如果运行到这里,说明钱包里没有钱,直接返回null。
return null;
}
}
现在,不防试一试我们自己的钱包类Wallet
:
public class Test {
// 钱。
static class Money {
private int num;
public Money(int num) {
this.num = num;
}
}
public static void main(String[] args) {
// 创建一个钱包,并且只允许这个钱包防止 Money 类型的数据。
Wallet<Money> wallet = new Wallet<>();
// 放入2张钱。
wallet.put(new Money(10));
wallet.put(new Money(50));
// 获取一张钱
// 获取的这张钱都不用强制转换,就可以直接使用。
// 通过泛型,这样就进一步减少了可能出现的程序错误。
// 并且代码更加简洁清晰。
Money money = wallet.get();
}
}
四、类型限定
在我们声明泛型
时,假如我们希望这个类型只能是某个类以及这个类的子类的类型,这时候我们可以这样来声明我们的类:
// 限定 类型 T 只能是 Money 类以及其子类。
class Wallet<T extends Money> {
// ...
}
五、泛型方法
如果我们类上没有指定类型,仅在方法上也是可以写泛型
方法的:
// 方法 make 定义个了一个类型 A ,并且方法返回参数被设定为 A
// 这样的方法可以被任何类型引用接收,将会自动将返回值转换为引用的类型
// (实际上就是里面某处进行了转换到A指定的处理)。
// 如果返回的值不能被转换为对应的引用类型,将会爆转换异常错误。
// 这种写法了解就好。
public static <A> A make() {
return (A)new Money(100);
}
举例使用这个方法:
public static void main(String[] args) {
Object o = make(); // Object 是所有类的超类,转换为 Object 没有问题。
Money m = make(); // 我们方法里面返回的就是Money,这里用Money接收没问题。
Test t = make(); // 编译时没有问题,当运行时就会出现转换错误,我们不能把返回的Money转为Test类型噻。
}