前言
crud 写多了才发现:泛型用对了是神器,用错了是噩梦。在
写业务代码时,泛型是我们每天都在用的东西:
result<list<userdto>> getusers(); baseservice<user, userdto> service; response<r<list<order>>> orders();
看起来很标准,但实际项目中,泛型相关的坑一踩一个准:
- 明明定义了泛型上限,序列化时却变成了
linkedhashmap - 泛型擦除导致
instanceof判断失效 - 泛型方法里
new t()报编译错误 - 工具类封装时泛型参数对不上
- ……
今天盘一盘泛型封装中 8 个高频踩坑点,看完直接落地。
1. 坑一:泛型擦除——instanceof判断永远为 false
常见写法
public class response<t> {
private t data;
public boolean islist() {
// 以为是 list 就返回 true?
return data instanceof list; // 永远 false!
}
}
问题在哪
运行期泛型会被擦除为 object,list<t> 会被擦除成 list,根本不存在 list<string> 这种具体类型。
所以 data instanceof list<string> 语法上就是错的,编译器直接报错。
正确做法
方案一:通过传入 class 参数判断
public class response<t> {
private t data;
private class<t> clazz;
public response(class<t> clazz) {
this.clazz = clazz;
}
public boolean islist() {
return list.class.isassignablefrom(clazz);
}
}
// 使用
response<list<userdto>> response = new response<>(new typetoken<list<userdto>>(){}.gettype());
方案二:用 typereference 保留泛型信息(json 序列化场景)
public class result<t> {
private t data;
// 配合 jackson 使用
public static <t> result<t> fromjson(string json, typereference<result<t>> typeref) {
try {
return new objectmapper().readvalue(json, typeref);
} catch (exception e) {
throw new runtimeexception(e);
}
}
}
// 调用
result<list<userdto>> result = result.fromjson(json,
new typereference<result<list<userdto>>>() {});
2. 坑二:工具类封装时泛型参数"对不上"
常见写法
public class resultutil {
public static <t> result<t> success(t data) {
result<t> result = new result<>();
result.setdata(data);
result.setcode(200);
return result;
}
}
// 调用
list<userdto> users = userservice.list();
result<list<userdto>> result = resultutil.success(users); // 完美
这看起来没问题。但如果这样呢?
public static <t> t getdata(result<t> result) {
if (result.getcode() == 200) {
return result.getdata(); // ok
}
return null; // 这里有问题吗?
}
问题在哪
当 t 是基本类型包装类(如 integer、long)时,返回 null 可能导致 npe。
正确做法
public static <t> t getdataorthrow(result<t> result) {
if (result.getcode() != 200) {
throw new businessexception(result.getmessage());
}
return result.getdata(); // 非空,编译器保证
}
// 或者返回空对象而非 null
public static <t> t getdata(result<t> result, t defaultvalue) {
if (result.getcode() != 200) {
return defaultvalue;
}
return result.getdata();
}
3. 坑三:new t()永远编译不过
常见写法
public class baseservice<t> {
public t createentity() {
// 想动态创建实例
return new t(); // 编译错误!
}
}
问题在哪
泛型擦除后,运行时根本不知道 t 是什么类型,无法调用构造函数。这是 java 类型系统的限制。
正确做法
方案一:通过 class 对象创建
public class baseservice<t> {
private final class<t> entityclass;
public baseservice(class<t> entityclass) {
this.entityclass = entityclass;
}
public t createentity() {
try {
return entityclass.getdeclaredconstructor().newinstance();
} catch (exception e) {
throw new runtimeexception("创建实例失败", e);
}
}
}
// 使用
userservice userservice = new userservice(user.class);
user user = userservice.createentity();
方案二:用反射工具类封装(推荐)
public class beanutil {
public static <t> t newinstance(class<t> clazz) {
try {
return clazz.getdeclaredconstructor().newinstance();
} catch (exception e) {
throw new illegalstateexception("无法创建实例: " + clazz.getname(), e);
}
}
public static <t> t copyproperties(object source, class<t> targetclass) {
t target = newinstance(targetclass);
beanutils.copyproperties(source, target);
return target;
}
}
4. 坑四:泛型上限没设对,导致类型转换 classcastexception
常见写法
// 随便定义一个泛型
public class dataholder<t> {
private t data;
public void process() {
// 假设需要调用 comparable 方法
comparable<t> comparable = data; // 可能出问题
comparable.compareto(data); // 如果 t 是 string,ok;如果是 user?user 没实现 comparable
}
}
问题在哪
没有约束 t 的上限,任何类型都能传,但代码里可能需要特定能力(如 comparable、serializable)。
正确做法
明确泛型上限
// 限定 t 必须实现 comparable
public class dataholder<t extends comparable<t>> {
private t data;
public t max(t other) {
return data.compareto(other) > 0 ? data : other; // 编译器保证安全
}
}
// 限定 t 必须实现序列化
public class cachewrapper<t extends serializable> {
private t data;
}
// 限定多重上限
public class processor<t extends number & comparable<t>> {
public double doublevalue(t value) {
return value.doublevalue();
}
}
常见场景:service 层基类
// 基础 service,限定 entity 必须继承 baseentity
public abstract class baseservice<t extends baseentity, dto> {
protected abstract mapper<t> getmapper();
public dto getbyid(long id) {
t entity = getmapper().selectbyid(id);
return converttodto(entity); // entity 一定有 id、createtime 等
}
protected abstract dto converttodto(t entity);
}
// 子类实现
public class userserviceimpl extends baseservice<user, userdto> {
@override
protected mapper<user> getmapper() {
return usermapper;
}
@override
protected userdto converttodto(user user) {
// user 一定有 getid(),因为继承了 baseentity
return userdto.builder()
.id(user.getid())
.name(user.getname())
.build();
}
}
5. 坑五:泛型方法定义错误,调用时类型推断失败
常见写法
public class converter {
// 以为是泛型方法,实际上不是
public static t convert(object source) {
return (t) source; // 编译警告,运行时可能 classcastexception
}
}
// 调用
string str = converter.convert(someobject); // 谁知道转成啥?
问题在哪
这个 t 不是方法级别泛型,而是类级别泛型。如果类没有声明 <t>,这里的 t 就是普通的类型参数(虽然也能编译,但语义完全错误)。
正确做法
正确的泛型方法
public class converter {
// 正确的泛型方法:<t> 是方法声明的一部分
public static <t> t convert(object source, class<t> targetclass) {
if (source == null) {
return null;
}
return targetclass.cast(source);
}
// 更安全的版本
public static <t, s> t convert(s source, function<s, t> converter) {
if (source == null) {
return null;
}
return converter.apply(source);
}
}
// 使用
string str = converter.convert(someobject, string.class);
userdto dto = converter.convert(user, userdto::todto);
复杂场景:返回多种类型的泛型方法
// 业务场景:统一处理成功/失败返回
public class apiresult {
public static <t> t getorthrow(apiresponse<t> response) {
if (!response.issuccess()) {
throw new apiexception(response.getcode(), response.getmessage());
}
return response.getdata();
}
// 配合 optional 使用
public static <t> optional<t> tooptional(apiresponse<t> response) {
if (response.issuccess() && response.getdata() != null) {
return optional.of(response.getdata());
}
return optional.empty();
}
}
6. 坑六:泛型通配符? extends和? super傻傻分不清
常见写法
// 读取数据时用 extends
public void read(list<? extends object> list) {
object item = list.get(0); // 读 ok
list.add(new object()); // 写?编译错误
}
// 写入数据时用 super
public void write(list<? super string> list) {
list.add("hello"); // 写 ok
string item = list.get(0); // 读?需要强制转型
}
问题在哪
搞不清 pecs 原则(producer extends, consumer super):
- 读取数据(生产者)→ 用
? extends - 写入数据(消费者)→ 用
? super
正确做法
记住 pecs 原则
// 生产者:用 extends,只能读
public double sumofprices(list<? extends product> products) {
double total = 0;
for (product p : products) { // 读 ok
total += p.getprice();
}
// products.add(new product()); // 编译错误,不能写
return total;
}
// 消费者:用 super,只能写
public void addnumbers(list<? super integer> list) {
list.add(1); // 写 ok
list.add(2);
// integer num = list.get(0); // 读出来是 object
}
// 既读又写?别用通配符
public <t> void copy(list<t> dest, list<? extends t> src) {
for (t item : src) { // src 是生产者,可以读
dest.add(item); // dest 是消费者,可以写
}
}
实际业务场景
// dto 转换:源列表是生产者,目标列表是消费者
public <s, t> void convertlist(list<s> sources, list<t> targets,
function<s, t> converter) {
for (s source : sources) {
targets.add(converter.apply(source));
}
}
// 使用
list<user> users = usermapper.selectlist();
list<userdto> dtos = new arraylist<>();
convertlist(users, dtos, user::todto);
7. 坑七:泛型与序列化冲突,返回给前端变成了 linkedhashmap
常见写法
public class result<t> {
private t data;
// 序列化给前端
public string tojson() {
return new objectmapper().writevalueasstring(this);
}
}
// 接口
@getmapping("/user")
public result<userdto> getuser() {
result<userdto> result = new result<>();
result.setdata(userdto);
return result;
}
前端收到的 json:
{
"data": {
"name": "张三",
"id": 1
}
}
这看起来没问题。但如果前端拿到的是 list<userdto> 呢?
问题在哪
当 t 是泛型集合时,jackson 默认反序列化会丢失具体类型信息,反序列化成 linkedhashmap 而不是具体 dto。
// 后端
result<list<userdto>> result = new result<>();
result.setdata(arrays.aslist(userdto1, userdto2));
// 前端收到
{
"data": [
{"name": "张三", "id": 1}, // 不再是 userdto
{"name": "李四", "id": 2}
]
}
正确做法
方案一:用 typereference 显式指定泛型
public class result<t> {
public string tojson() {
try {
objectmapper mapper = new objectmapper();
mapper.registermodule(new javatimemodule());
return mapper.writevalueasstring(this);
} catch (exception e) {
throw new runtimeexception(e);
}
}
// 泛型反序列化方法
public static <t> t fromjson(string json, typereference<t> typeref) {
try {
return new objectmapper().readvalue(json, typeref);
} catch (exception e) {
throw new runtimeexception(e);
}
}
}
// 后端给前端:直接序列化,不需要改动
// 前端拿到字符串后:
result<list<userdto>> result = result.fromjson(jsonstring,
new typereference<result<list<userdto>>>() {});
方案二:用 @jsontypeinfo 标记具体类型
@jsontypeinfo(use = jsontypeinfo.id.minimal_class)
public class result<t> {
private t data;
}
// 序列化成
{
"data": {
"@c": ".userdto",
"name": "张三"
}
}
方案三:返回 responseentity(spring 官方推荐)
@getmapping("/users")
public responseentity<result<list<userdto>>> getusers() {
result<list<userdto>> result = result.success(userservice.list());
return responseentity.ok(result);
}
8. 坑八:泛型嵌套太深,代码可读性灾难
常见写法
// 四层泛型嵌套,你能一眼看出 data 是什么吗? response<result<page<list<userdto>>>> result = userservice.query(request); // 访问数据时 list<userdto> users = result.getdata().getdata().getdata().getrecords();
问题在哪
泛型是为了类型安全,但如果嵌套太深,反而降低了可读性,而且修改维护时容易出错。
正确做法
方案一:抽取中间类型
// 第一层:接口返回统一封装
public class apiresponse<t> {
private int code;
private string message;
private t data;
}
// 第二层:分页数据统一封装
public class pageresult<t> {
private list<t> records;
private long total;
private int pagenum;
private int pagesize;
}
// 简化后的调用
apiresponse<pageresult<userdto>> result = userservice.query(request);
pageresult<userdto> page = result.getdata();
list<userdto> users = page.getrecords();
方案二:用 optional 消除空判断
public class result<t> {
private t data;
public optional<t> getoptionaldata() {
return optional.ofnullable(data);
}
}
// 使用
user.getoptionaldata()
.map(pageresult::getrecords)
.orelse(collections.emptylist());
方案三:工具方法封装常用路径
public class resulthelper {
public static <t> list<t> getrecordsorempty(result<pageresult<t>> result) {
if (result == null || result.getdata() == null) {
return collections.emptylist();
}
return result.getdata().getrecords();
}
}
// 调用
list<userdto> users = resulthelper.getrecordsorempty(result);
最佳实践总结
| 坑点 | 问题 | 解决方案 |
|---|---|---|
| instanceof 失效 | 泛型擦除 | 用 class 或 typereference 判断 |
| 工具类泛型失效 | 泛型参数对不上 | 显式传入 class 或用函数式接口 |
| new t() 编译错误 | 类型擦除限制 | 通过 class.newinstance() 或构造函数引用 |
| classcastexception | 泛型上限未设 | 用 约束 |
| 类型推断失败 | 泛型方法定义错误 | 放在返回类型前 |
| extends/super 混淆 | pecs 原则不清 | 记住:读用 extends,写用 super |
| 序列化变成 map | 泛型信息丢失 | 用 typereference 或 responseentity |
| 泛型嵌套太深 | 可读性差 | 抽取中间类型 + 工具方法封装 |
泛型封装黄金法则
1. 永远不要 new t()
2. 永远不要写 instance of t
3. 永远明确泛型上限
4. 永远记住 pecs 原则
5. 永远用 typereference 处理 json 序列化
记住:泛型是给编译器用的,不是给运行时用的。想清楚这一点,大部分坑都能避开。
到此这篇关于springboot泛型封装的坑的文章就介绍到这了,更多相关springboot泛型封装内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论