最近在网上看到很多新手不太理解 Java 中的泛型,尤其是对于源码中各种通配符 “?”、“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);//即使如此它还是无法限制,只能起到提示作用
}
你看上面的代码其实治标不治本,我们声明了 listString 是想让它只存储 String 类型,但是我们仍然可以存储非 String 类型的数据,而且更为重要的是这种类型转换异常通常只有在运行时才会被发现。我们需要一种机制能强制性的让我们只能存储对应类型的元素,否则编译就不通过,所以泛型出现了。
事实上泛型也是我们刚刚的思路,在实例化时给集合分配一个类型,限定一个集合只能存储我们分配的类型的元素
public static void main(String[] args) {
ArrayList 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 类型的
其实到这里我还没有给泛型下定义,别急,看完下一节 泛型类
说到泛型类,最典型的例子就是上面我们说的 ArrayList 了。你有没有想过,为什么给 ArrayList 指定泛型之后,就只能存储指定类型,get(0) 获取元素返回值就是指定的那个泛型类型?看下 ArrayList 部分源码
//类定义
public class ArrayList extends AbstractList implements List
//添加元素方法
public boolean add(E e) {...}
//获取元素方法
public E get(int index) {...}
可以看到 ArrayList 在定义的时候指定了一个泛型
当然我们也可以在一个类中定义多个泛型参数,比如 HashMap
public class HashMap extends AbstractMap implements Map
定义:泛型的本质就是把类型参数化,所操作的数据类型被指定为参数,根据动态传入进行处理
上面我们看到的是 JDK 源码中泛型类,当然我们自己也可以定义泛型类,最常见的就是我们曾经封装的 web 应用后端返回结果。
public class ResultHelper implements Serializable {
private T data;
private boolean success;
private Integer code;
private String message;
private ResultHelper() {}
public static ResultHelper wrapSuccessfulResult(T data) {
ResultHelper result = new ResultHelper();
result.data = data;
result.success = true;
result.code = 0;
return result;
}
}
这是很多公司会用的工具类,封装一个这样的数据结构给前端(不过现在大部分企业已经不这么用了)。这样一来,我的 wrapSuccessfulResult 泛型方法的参数泛型 T 可以接收调用者任何参数,无论是订单数据还是商品数据等等,都可以封装。
有时候开发中我们会有这样一种需求,根据方法传入的参数类型不同,返回不同的返回值类型。上面所说的自定义泛型类 wrapSuccessfulResult 方法就是典型的泛型方法,它只有一个泛型参数,我们还可以使用多个泛型参数:
public static List convertListToList(List input, Class clzz) {
List 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 都是开放的,随便调用者传递什么类型。
“?” 代表不确定的类型,比如我们公司 APP 有一个订单列表/详情的需求,我们都知道订单有待发货、待收货、已退款等不同页面,不同状态页面的数据又不一样。如果把所有字段都放在一个类中,那样的设计太难受了,代码看起来更难受(实际上第一版就是这么干的,后来是我改的)。比如待收货有发货时间,待支付就没有,如果你用一个有发货时间字段的类来作为待支付详情的数据结构,那就不合适。所以我写了一个父类 AppOrderResponse 把所有页面共有的字段(订单id、订单编号、订单状态、下单时间等)放在父类,其他独有的再扩展子类,继承关系为:
这样根据不同状态返回对应的类型数据就行了,比如待发货列表就返回 AppOrderWaitDeliveredResponse 泛型类型,待收货列表就返回 AppOrderDispatchedResponse 泛型类型,不过我们一个接口要同时返回四种可能的类型,这该怎么办呢?也许你可能会这么想
public IPage list(Page page, AppOrderQueryRequest request) {
IPage demo1 = new Page<>();
IPage demo2 = new Page<>();
//return demo1;//报错
return demo2;//报错
}
将返回值定义为 IPage
public IPage> list(Page page, AppOrderQueryRequest request) {
return appOrderService.list(page, request);
}
这样一来我们使用通配符 “?” 之后返回任何 IPage 泛型都可以。但是这显然不合适,因为我们的类型是 AppOrderResponse ,不可能允许程序返回一个不属于 AppOrderResponse 的泛型类型。所以我们可以使用泛型的上界下界来控制
正如上面订单列表的例子,我们应该限定返回值的泛型仅为 AppOrderResponse 或者其子类,可以这么写
public IPage extends AppOrderResponse> list(Page page, AppOrderQueryRequest request) {
return appOrderService.list(page, request);
}
这种写法叫做指定泛型的上界(上限),我们不能直接用 IPage
其实这从 extends 和 super 关键字很容易理解
IPage extends AppOrderResponse> //表示泛型最高类型是 AppOrderResponse,只能是它或它的子类
IPage super AppOrderResponse> //表示泛型最低类型是 AppOrderResponse。只能是它或它的父类
上面我们都是把无界通配符用在返回值,当然无界通配符也是可以用在方法参数上的
public void test1(List extends AppOrderResponse> list){
list.add(new AppOrderDispatchedResponse());//添加元素报错,因为我们传入的可能是 List
list.add(new AppOrderResponse());//添加元素报错,因为我们传入的可能是 List
list.add(null);//这是唯一可以添加的元素 null
AppOrderResponse appOrderResponse = list.get(0);//接受元素类型为 AppOrderResponse
}
当使用上界参数时,不可以对参数进行新增元素。因为参数需要的泛型是 AppOrderResponse 及其子类,我们在方法体中添加元素会报错是因为传入进来的有可能是 AppOrderResponse,也有可能是它的子类集合。比如我们传入的参数是 List
而当我们获取元素时,这个元素一定是 AppOrderResponse 或者其子类类型的,所以我们可以用 AppOrderResponse 来接受,用父类引用指向子类对象。
泛型上界参数在方法内只能读取,不能写入
public void test2(List super AppOrderResponse> list){
list.add(new AppOrderResponse());//可以添加
list.add(new AppOrderDispatchedResponse());//可以添加
Object object = list.get(0);//接受元素的类型为 Object
}
使用下界参数时,由于参数需要的泛型是 AppOrderResponse 及其父类,那么我们添加 AppOrderDispatchedResponse 类型的元素当然是可以的,因为 AppOrderDispatchedResponse 是子类,肯定是 AppOrderResponse 的类型,这也就是父类引用指向子类对象。
而当我们获取元素时,由于 list 中的元素类型是 AppOrderResponse 父类类型,所以我们必须得用 Object 来接受,这样一来其实没有意义,我拿一个 Object 干嘛呢。
泛型下界参数在方法内只能写入,不能读取
我相信这是广大同学最容易混淆的地方,毕竟源码中到处都是这些通配符,也看不出有什么区别。其实 T、E、R、K、V 对于程序运行没有区别,定义泛型的时候用 A-Z 中任何一个字母都可以,只不过我们上面的几个是约定俗成的,也算一种规范。
那无界通配符 “?” 和它们有啥区别呢?
比如我们上面 泛型类 的代码示例,用 T 来定义一个泛型,并且可以在代码中对 T 进行操作。而 T 不可以单独作为方法形参,只能在定义的泛型类中或者定义泛型方法才能作为方法形参。
public class ResultHelper implements Serializable {
private T data;
...
data.toString();
data.equals(obj);
...
public static ResultHelper wrapSuccessfulResult(T data) {}
比如我们在 无界通配符 “?” 的代码示例,即使不在泛型类中,"?" 也可以作为方法形参,定义方法返回值等,但是我们不能对 “?” 进行单独定义、操作
private List> list;
private ? data;//报错
public class OrderRequest> {}//报错
public void test(? item){}//报错
所谓的泛型擦除其实很简单,简单来说就是泛型只在编译时起作用,运行时泛型还是被当成 Object 来处理,示例代码
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
list.add("wan");//add 方法形参类型为 String
String s = list.get(0);//get方法返回值类型为 String
ArrayList 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 源码、框架源码,看大牛是如何在框架中使用的。