最近在学习中,我对于泛型这个知识点其实一直比较模糊,对于源码的各种通配符“?”,“T”,“S”,"R"等,都不了解其含义,更加不要说怎么去使用泛型。所以,我专门写了一篇文章,来从头开始透彻的分析Java中的泛型,并结合项目实际的应用场景,让我自己和读者都可以更加深刻的来理解泛型。我的这篇文章,采用的是问题引导式,层层递进,我相信只要读者可以跟着我的思路走,必定可以对泛型有一个新的认识。
在谈泛型之前,我们先来看一段JDK5之前没有泛型的代码
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(521);//添加 Integer 类型元素
list.add("wan");//添加 String 类型元素
list.add(true);//添加 Boolean 类型元素
list.add('a');//添加 Character 类型元素
Object item1 = list.get(0);//只能用 Object 接受元素
list.forEach(item -> {
//使用 item,这里的 item 类型是 Object,由于不知道 item 的确切类型,我们需要判断之后强转
if (item instanceof Integer) {
//执行业务...
} else if (item instanceof String) {
//执行业务...
} else if (item instanceof Boolean) {
//执行业务...
} //继续判断类型...
});
}
没有泛型的时候,我们声明的List集合默认是可以存储任意类型元素的,乍一看你可能还会觉得挺好,这样功能强大,啥类型都可以存储。但是......在开发的时候由于不知道集合中元素的确切类型,遍历的时候我们拿到的item其实都是Object类型,如果要使用就必须强转,强转就必须得判断当前元素的具体类型,否则直接使用强转很可能会发生类型转换异常。这样就会让开发很不方便,每次都要额外做判断工作。
总结起来就不一句话:它不安全!
那么你可能已经想到了,我们在业务中不要把全部数据都存放在一个List就行了,在代码中定义多个List分类型使用。就像这样:
public static void main(String[] args) {
ArrayList listInteger = new ArrayList();
ArrayList listString = new ArrayList();
ArrayList listBoolean = new ArrayList();
//...这样就可以在不同的 list 中存入对应的类型数据
//——————————————————————分割线————————————————————
listString.add(121);//即使如此它还是无法限制,只能起到提示作用
}
不知道你们可不可以想到这种方法,反正我是想到了哈哈哈,我一开始还以为这个方法很nice,但是当我了解到更多时,我发现这个方法其实是一坨勾丝,治标不治本。我们声明了listString是想让它只存储String类型,但是其实它仍然可以存储非String类型的其它类型的数据,而且更为重要的是这种类型转换异常通常只有在运行时才会被发现。我们需要一种机制能够强制性的让我们只能存储对应类型的元素,否则编译就不通过,所以泛型出现了。
事实上,泛型也是我们刚刚的思路,就是在实例化的时候给集合分配一个类型,限定一个集合只能存储我们分配的类型的元素。比如像这样子:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("wan");
list.add(521);//编译报错,只能存储 String 元素
String str = list.get(0);//直接用 String 类型接受元素
list.forEach(item -> {
//这里 item 类型就是 String
});
}
有了泛型的指定,我们声明的list就只能存储规定类型String,当存储其他类型的元素时,编译器就会直接给我们报错(可以在IDEA开发环境中看看add方法,提示的参数类型就是String),这样雷类型不匹配的问题就在编译时就可以检查出来,而不会在运行时才抛出异常。而且当我们进行遍历,获取元素等操作时,get方法返回值就是String类型的。
所以总结一下泛型的好处
(1)编译期类型安全
(2)避免了强制类型转换运行时异常
(3)同一个类可以操作多种数据类型,代码复用
其实到这里我还没有给泛型下定义,别急,看完下一节泛型类再说。
说到泛型类,最典型的例子就是上面我们说的ArrayList了。你有没有想过,为什么给ArrayList指定泛型后,就只能存储指定的类型,get(0)获取元素返回值就是指定的那个泛型类型?你看一下ArrayList源码就知道了:
//类定义
public class ArrayList<E> extends AbstractList<E> implements List<E>
//添加元素方法
public boolean add(E e) {...}
//获取元素方法
public E get(int index) {...}
可以看到ArrayList在定义的时候制定了一个泛型<E>,并且下面的添加元素,获取元素等方法都是对这个E进行操作,我当时其实对这个比较懵.....这个E是什么鬼?其实这个E就是我们实例化ArrayList的时候指定的类型,当我们指定String,add方法的形参和get方法的返回值就是String类型。当我们指定Integer,add方法的形参和get方法的返回值就是Integer类型。这样一来,ArrayList这个类就被参数化了,当实例化ArrayList时传入不同的泛型就可以操作不同的类型。
当然我们也可以在一个类中个定义多个泛型参数,比如HashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>
说到这里,是时候回答一下前面我抛出的一个问题了,就是泛型的定义是什么?其实,泛型的本质就是把类型参数化,所操作的数据类型被指定为参数,根据动态传入进行处理。
上面我们看到的是JDK源码中泛型类,当然我们自己也可以定义泛型类,最常见的 就是我们曾经封装的 web应用后端返回结果。
public class ResultHelper<T> implements Serializable {
private T data;
private boolean success;
private Integer code;
private String message;
private ResultHelper() {}
public static <T> ResultHelper<T> wrapSuccessfulResult(T data) {
ResultHelper<T> result = new ResultHelper<T>();
result.data = data;
result.success = true;
result.code = 0;
return result;
}
}
这是做项目经常会用的工具类,封装一个这样的数据结构给前端(不过听说现在大部分企业已经不这么用了)。这样一来,我的wrapSuccessfulResult 泛型方法的参数泛型T可以接受调用者任何参数。无论是订单数据还是商品数据等等,都可以封装。
有时候开发中我们会有这样一个需求,根据方法传入的参数类型不同,返回不同的返回值类型。前面所说的自定义泛型类wrapSuccessfulResult 方法就是典型的泛型方法,它只有一个泛型参数,我们还可以使用多个泛型参数:
public static <E, T> List<T> convertListToList(List<E> input, Class<T> clzz) {
List<T> output = Lists.newArrayList();
if (CollectionUtils.isNotEmpty(input)) {
input.forEach(source -> {
T target = BeanUtils.instantiate(clzz);
BeanUtils.copyProperties(source, target);
output.add(target);
});
}
return output;
}
例如上面这个方法(不用太过注重方法体),它的作用就是把一个类型E的List转换为另一个类型T的List。这里的 E和T都是开放的,随便调用者传递什么类型。
"?" 代表不确定的类型,比如我大二的时候曾经写过一个后端的程序,那个程序里面有一个订单列表/详情的需求,我们都知道订单有待发货、待收货、已退款等不同页面,不同状态页面的数据又不一样。如果把所有字段都放在一个类中,那样的设计太难受了,代码看起来更难受(实际上那个程序刚开始就是这么干的,后来是我改的)。比如待收货有发货时间,待支付就没有,如果你用一个有发货时间字段的类来作为待支付详情的数据结构,那就不合适。所以我写了一个父类 AppOrderResponse 把所有页面共有的字段(订单id、订单编号、订单状态、下单时间等)放在父类,其他独有的再扩展子类,继承关系为:
这样根据不同状态返回对应的类型数据就行了,比如待发货列表就返回 AppOrderWaitDeliveredResponse 泛型类型,待收货列表就返回 AppOrderDispatchedResponse 泛型类型,不过我们一个接口要同时返回四种可能的类型,这该怎么办呢?也许你可能会这么想:
public IPage<AppOrderResponse> list(Page<AppOrderResponse> page, AppOrderQueryRequest request) {
IPage<AppOrderDispatchedResponse> demo1 = new Page<>();
IPage<AppOrderWaitDeliveredResponse> demo2 = new Page<>();
return demo1;//报错
return demo2;//报错
}
将返回值定义为 IPage<AppOrderResponse> 类型,这样我就可以返回各种类型了,但其实这样会报错。AppOrderResponse 是 AppOrderWaitDeliveredResponse 的父类,但是 IPage<AppOrderResponse> 并不是 IPage<AppOrderWaitDeliveredResponse> 的父类,泛型中的继承不是你想的那样。此时就需要使用通配符 "?" 来解决。
public IPage<?> list(Page<AppOrderResponse> page, AppOrderQueryRequest request) {
return appOrderService.list(page, request);
}
这样一来我们使用通配符 "?" 之后返回任何 IPage 泛型都可以。但是这显然不合适,因为我们的类型是 AppOrderResponse ,不可能允许程序返回一个不属于 AppOrderResponse 的泛型类型。所以我们可以使用泛型的上界下界来控制。
正如上面订单列表的例子,我们应该限定返回值的泛型仅为 AppOrderResponse 或者其子类,可以这么写:
public IPage<? extends AppOrderResponse> list(Page<AppOrderResponse> page, AppOrderQueryRequest request) {
return appOrderService.list(page, request);
}
这种写法叫做指定泛型的上界(上限),我们不能直接用 IPage<AppOrderResponse> 表示上界,但是可以使用 IPage<? extends AppOrderResponse>
其实这从 extends 和 super 关键字很容易理解:
IPage<? extends AppOrderResponse> //表示泛型最高类型是 AppOrderResponse,只能是它或它的子类
IPage<? super AppOrderResponse> //表示泛型最低类型是 AppOrderResponse。只能是它或它的父类
我相信这是广大同学最容易混淆的地方,毕竟源码中到处都是这些通配符,也看不出有什么区别。其实 T、E、R、K、V 对于程序运行没有区别,定义泛型的时候用 A-Z 中任何一个字母都可以,只不过我们上面的几个是约定俗成的,也算一种规范。
所谓的泛型擦除其实很简单,简单来说就是泛型只在编译时起作用,运行时泛型还是被当成 Object 来处理,示例代码:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("wan");//add 方法形参类型为 String
String s = list.get(0);//get方法返回值类型为 String
ArrayList<Integer> list2 = new ArrayList<>();
System.out.println("list 和 list2 类型相同吗:" + (list.getClass() == list2.getClass()));//true 两个 ArrayList 是同一个类型的
Method[] methods = list.getClass().getMethods();
for (Method method : methods) {
method.setAccessible(true);
if (method.getName().equals("add")) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {
for (Class<?> parameterType : parameterTypes) {
System.out.println("add(E e) 形参 E 的类型为:" + parameterType.getName());//泛型的参数 E 运行时是 Object 类型
}
}
} else if (method.getName().equals("get")) {
Class<?> returnType = method.getReturnType();
System.out.println("E get(int index) 的返回值 E 的类型为:" + returnType.getName());
}
}
}
运行结果如下:
list 和 list2 在运行时类型相同吗:true
add(E e) 形参 E 在运行时的类型为:java.lang.Object
E get(int index) 的返回值 E 在运行时的类型为:java.lang.Object
可以看到我们实例化 ArrayList 时虽然传入不同的泛型,但其实它们仍然还是同一个类型。对于 add 方法的形参和 get 方法的返回值,按道理说我们指定的泛型是 String 那么打印出来应该是 String 才对,但是这里运行时得到的却都是 Object,所以这就足以证明了,泛型在编译期起作用,运行时一律被擦除当做 Object 看待,这就是泛型擦除。
泛型这个东西理解起来其实真的很简单,难的是如何把它用好,这个需要很强的编程功底、设计模式,我的建议是多看 JDK 源码、框架源码,看大牛是如何在框架中使用的。