这其实是我遇到的一个线上bug,在这里分享给大家。
如果是用反射,那就很简单了,毕竟泛型只是在编译期进行约束,对运行期是无能为力的。
想想看,如果不使用反射,有没有办法做到呢?
问题起因
在我们公司的实际业务中,有一段类似于这样逻辑的代码,文章最后会放出做测试构造的getList()方法:
/**
* 主要业务逻辑
*/
public static void main(String[] args) {
// 从数据库查询数据列表,不用关注里面的实现细节
List<DataBO> list = getList();
// 获取所有“a”字段的值的集合
List<Integer> integerList = toList(list, "a");
if (integerList.contains(1)) {
System.out.println("集合里包含1,处理对应的逻辑");
} else {
System.out.println("集合里不包含1,处理对应的逻辑");
}
}
/**
* 这是公司提供的一个公共工具方法,获取集合中,每个对象的某个字段的值的集合
*
* @param list 数据对象集合
* @param key 字段
* @return 值的集合
*/
public static <T> List<T> toList(List<DataBO> list, String key) {
return list.stream()
.filter(x -> x.get(key) != null)
.map(x -> (T)x.get(key))
.collect(Collectors.toList());
}
其中的DataBO
对象简化如下:
public class DataBO {
/** 数据库的一条数据,key是列,value是值 */
private Map<String, Object> map = new HashMap<>();
public Object get(String key) {
return map.get(key);
}
public void set(String key, Object value) {
map.put(key, value);
}
@Override
public String toString() {
return "DataBO{" + "map=" + map + '}';
}
}
原本我这里的业务需求是,取列表数据中,所有“a”字段的值出来,判断其中是否含有1。
已知数据库里“a”字段定义为int类型,并且确认了有一条数据在“a”字段上存的是1。但是代码上线一跑,出bug了。
查出来怎么就走到“不包含1”的分支里去了呢?也没有报错,难道底层服务的getList()方法有什么特殊处理,把数据库a=1的那条数据给过滤掉了吗?
问题定位
于是我加了点日志,把list
和intergerList
的元素打印出来,看看里面到底存了什么东西。于是又上线一版,观察一看,神奇的事情出现了,里面明明有1啊??!为啥会走到下面“不包含1”的分支呢?见鬼了!
于是我只能本地debug了一下,才发现数据库查到的集合里,“a”字段返回的是个字符串"1"!而ArrayList的contains()方法,底层是用equals()去比较是否存在的。"1".equals(1),结果肯定是false,所以认为不存在。
好吧,虽然数据库的“a”字段定义为int类型,但是底层服务估计哪里有bug,把Integer类型的字段,转换成了String类型返回给上层服务了。
但转念一向,不对啊,我明明定义的是List<Integer>
类型的变量,如果是这样的话,就算查出来"a"字段不是个Integer类型的值,那toList()方法也应该是抛个java.lang.ClassCastException
才对,怎么可能正常往下走呢?List<Integer>
变量指向的对象里,为什么会存进去一个字符串呢?为什么toList()方法的.map(x -> (T)x.get(key))
这一行没有报错呢?
问题解析
问题很明显就是出在了toList()方法里,那个强制类型转换为泛型,并没有生效。开头我们说了,java的泛型,只是在编译期进行约束,对运行期是无能为力的。那么我们首先就应该想到的就是java的泛型擦除机制,我们对demo类进行编译、再反编译看看。
反编译可以发现,原来toList()方法中,强制类型转换被擦除了。所以返回的其实并不是List<Integer>
对象,而是List对象,没有泛型限制。很明显是这个方法有bug,其实就是泛型方法使用错误了。
问题修复
本来这个线上bug到这里就已经搞清楚了,如果只是要快速修复上线也很容易就能解决,把toList()方法返回的集合改成List<String>
,然后判断集合是否包含字符串"1"就行。
但我们想,如果后面又有别的同事遇到这个问题了怎么办呢,也会一脸懵逼,最好还是希望toList()方法抛出个java.lang.ClassCastException
,而且还要做到原来这个方法的效果,该怎么修改这个方法呢?
我们可以增加一个参数,告诉方法你希望返回一个什么类型的值:
这样的话,如果toList()方法还是返回原来的List<Integer>
,就会抛异常:
而且如果前后限制的类型不一致,编译期也会报错,泛型就起作用了:
到此这个问题彻底解决。
补充下本文用于测试构造的getList()方法:
/**
* 查数据库,获取数据对象的集合
*
* @return 数据对象的集合
*/
public static List<DataBO> getList() {
// 这个list是从数据库查出来的
List<DataBO> list = new ArrayList<>();
DataBO db1 = new DataBO();
db1.set("a", "1");
DataBO db2 = new DataBO();
db2.set("a", 2);
list.add(db1);
list.add(db2);
return list;
}