告别重复代码!Java 泛型实战:统一结果封装 / 分页 / 对象拷贝,五大场景直接套用
引言
在Java开发中,泛型(Generics)是保障代码安全性、提升复用性的关键技术之一。它通过在编译阶段进行类型校验,彻底解决了传统集合框架中频繁类型转换导致的ClassCastException问题,同时让一套逻辑能够灵活适配多种数据类型,极大简化了代码结构。
很多开发者对泛型的认知停留在用
<T>代替具体类型,但泛型的核心价值远不止于此。它本质是一种类型参数化机制,将类型从硬编码变为动态传入,从而实现类型安全与代码复用的双重目标。
泛型通过以下机制解决这些问题:
- 编译期类型校验:在定义集合(如
List<User>)时指定类型,编译器自动拦截不符合类型的数据添加操作; - 自动类型推断:无需手动强制转换,
list.get(0)直接返回指定类型(如User),减少代码冗余; - 逻辑复用:一套泛型类/方法(如
Result<T>)可适配所有数据类型,避免重复开发。
|
语法形式 |
定位 |
作用 |
示例 |
|
|
类型参数声明 |
定义泛型变量,代表后续可传入的具体类型(T为约定俗成的占位符,可替换为E(元素)、K(键)、V(值)等) |
定义泛型类: |
|
|
参数化类型 |
表示特定类型的集合/对象,明确泛型变量的具体应用场景 |
(存储用户的列表)、 |
|
|
类型边界限制 |
约束泛型变量的范围,仅允许传入指定类及其子类,避免类型滥用 |
定义仅支持数字类型的泛型方法: |
实战场景
场景 1:API 统一返回结果封装(解决多类型结果冗余问题)
Web接口返回结果需包含状态码(code)、提示信息(msg)和业务数据(data),若为每个业务类型(如用户、订单、凭证)单独编写结果类(UserResult、OrderResult),会产生大量重复代码,且新增业务类型时需同步新增结果类,违反开闭原则。
定义泛型结果类Result<T>,用T表示动态变化的业务数据类型,通过静态工厂方法简化调用。
/**
* 通用API返回结果类
* @param <T> 业务数据类型(如User、List<Order>、Boolean等)
*/
public class Result<T> {
// 状态码:200(成功)、500(服务器错误)、400(参数错误)等
private int code;
// 提示信息:如“操作成功”“用户不存在”
private String msg;
// 业务数据:泛型类型,适配任意数据结构
private T data;
// 私有构造方法:避免外部直接创建,统一通过工厂方法调用
private Result(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
// 工厂方法:成功场景(无提示信息,默认“success”)
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
// 重载工厂方法:成功场景(自定义提示信息)
public static <T> Result<T> success(String msg, T data) {
return new Result<>(200, msg, data);
}
// 工厂方法:失败场景(自定义状态码和提示信息)
public static <T> Result<T> fail(int code, String msg) {
// 失败场景无业务数据,data设为null
return new Result<>(code, msg, null);
}
// getter方法(省略setter:结果类为不可变对象,避免后续修改)
public int getCode() { return code; }
public String getMsg() { return msg; }
public T getData() { return data; }
}
示例:
// 1. 返回单个用户信息(data为User类型)
User user = userService.getById(1L);
return Result.success(user);
// 2. 返回订单列表(data为List<Order>类型)
List<Order> orderList = orderService.listByUserId(1L);
return Result.success("查询订单成功", orderList);
// 3. 返回布尔状态(如“是否已收藏”,data为Boolean类型)
boolean isCollected = collectService.check(1L, 2L);
return Result.success(isCollected);
// 4. 返回失败结果(无业务数据)
if (user == null) {
return Result.fail(404, "用户不存在");
}
方案优势:
- 不可变设计:私有构造方法+仅提供
getter,避免结果对象被后续修改,保障数据安全性; - 灵活重载:通过工厂方法重载,适配默认提示、自定义提示、失败场景等不同需求;
- 类型安全:编译器自动校验
data类型,若将User传入Result<List<Order>>会直接编译报错,避免运行时异常。
场景 2:分页查询结果封装(解决分页逻辑重复问题)
分页查询是业务系统的基础需求,返回结果通常包含总条数(
total)、当前页数据(list)、页码(pageNum)、页大小(pageSize)。若为每个业务类型编写分页结果类(UserPageResult、VoucherPageResult),会导致分页逻辑与业务类型强耦合,新增业务时需重复编写分页结构。
定义泛型分页类PageResult<T>,用T表示当前页数据的类型,统一分页结构,适配所有业务的分页需求。
/**
* 通用分页查询结果类
* @param <T> 当前页数据的类型(如User、Voucher等)
*/
public class PageResult<T> {
// 总记录数:满足查询条件的所有数据条数
private long total;
// 当前页数据列表:泛型类型,适配不同业务数据
private List<T> list;
// 当前页码:如第1页、第2页
private int pageNum;
// 页大小:每页显示的数据条数,如10条/页
private int pageSize;
// 构造方法:直接暴露,便于外部传入分页参数
public PageResult(long total, List<T> list, int pageNum, int pageSize) {
this.total = total;
this.list = list;
this.pageNum = pageNum;
this.pageSize = pageSize;
}
// 静态工厂方法:简化对象创建(可选,根据团队习惯选择)
public static <T> PageResult<T> of(long total, List<T> list, int pageNum, int pageSize) {
return new PageResult<>(total, list, pageNum, pageSize);
}
// getter方法(省略setter:分页结果为查询结果,无需后续修改)
public long getTotal() { return total; }
public List<T> getList() { return list; }
public int getPageNum() { return pageNum; }
public int getPageSize() { return pageSize; }
}
场景 3:策略模式 + 泛型:通用数据处理器(解决多类型业务逻辑耦合问题)
系统中有多种凭证类型(电子券、纸质券、红包),每种凭证需执行校验合法性→发放凭证的逻辑,但不同凭证的校验规则和发放方式不同。若用
if-else判断凭证类型并执行对应逻辑,会导致代码臃肿),新增凭证类型时需修改原有逻辑,违反开闭原则。
结合策略模式,定义泛型处理器接口Handler<T>,为每种凭证类型实现独立的处理器,再通过处理器工厂动态匹配类型,实现新增类型仅需新增处理器的解耦效果。
/**
* 通用数据处理器接口(策略模式核心)
* @param <T> 处理的数据类型(如Ecoupon、PaperVoucher等)
*/
public interface Handler<T> {
/**
* 判断当前处理器是否支持该数据类型
* @param clazz 数据类型的Class对象
* @returntrue:支持;false:不支持
*/
boolean supports(Class<?> clazz);
/**
* 处理核心业务逻辑(校验+发放)
* @param data 待处理的数据(泛型类型,确保类型安全)
*/
void handle(T data);
}
实现具体凭证处理器(策略实现):
// 电子券处理器
@Component // 注入Spring容器,便于后续工厂自动获取
public class EcouponHandler implements Handler<Ecoupon> {
@Override
public boolean supports(Class<?> clazz) {
// 判断传入的Class是否为Ecoupon或其子类
return Ecoupon.class.isAssignableFrom(clazz);
}
@Override
public void handle(Ecoupon ecoupon) {
// 1. 电子券合法性校验(如券码是否有效、是否过期)
validateEcoupon(ecoupon);
// 2. 电子券发放逻辑(如写入用户券包、发送短信通知)
issueEcoupon(ecoupon);
}
// 电子券专属校验逻辑
private void validateEcoupon(Ecoupon ecoupon) {
if (ecoupon.getExpireTime().before(new Date())) {
throw new IllegalArgumentException("电子券已过期:" + ecoupon.getCode());
}
}
// 电子券专属发放逻辑
private void issueEcoupon(Ecoupon ecoupon) {
System.out.println("电子券发放成功:券码=" + ecoupon.getCode() + ",用户ID=" + ecoupon.getUserId());
}
}
// 纸质券处理器(同理,仅实现纸质券专属逻辑)
@Component
public class PaperVoucherHandler implements Handler<PaperVoucher> {
@Override
public boolean supports(Class<?> clazz) {
return PaperVoucher.class.isAssignableFrom(clazz);
}
@Override
public void handle(PaperVoucher paperVoucher) {
// 纸质券校验(如序列号是否存在)
validatePaperVoucher(paperVoucher);
// 纸质券发放(如记录发放日志、通知线下门店)
issuePaperVoucher(paperVoucher);
}
private void validatePaperVoucher(PaperVoucher paperVoucher) {
if (paperVoucher.getSerialNo() == null || paperVoucher.getSerialNo().length() != 10) {
throw new IllegalArgumentException("纸质券序列号无效:" + paperVoucher.getSerialNo());
}
}
private void issuePaperVoucher(PaperVoucher paperVoucher) {
System.out.println("纸质券发放成功:序列号=" + paperVoucher.getSerialNo() + ",用户ID=" + paperVoucher.getUserId());
}
}
实现处理器工厂(动态匹配策略):
@Service
public class HandlerManager {
// 自动注入Spring容器中所有Handler实现类(泛型通配符<?>表示“任意类型的Handler”)
@Autowired
private List<Handler<?>> handlers;
/**
* 通用处理方法:根据数据类型动态匹配处理器并执行逻辑
* @param data 待处理的数据(泛型类型T)
* @param <T> 数据类型
*/
@SuppressWarnings("unchecked") // 抑制“未检查的转换”警告(通过supports校验,转换安全)
public <T> void process(T data) {
if (data == null) {
throw new IllegalArgumentException("待处理数据不能为空");
}
// 获取数据的Class对象(如Ecoupon.class)
Class<?> clazz = data.getClass();
// 遍历所有处理器,找到支持当前类型的处理器
for (Handler<?> handler : handlers) {
if (handler.supports(clazz)) {
// 将Handler<?>强制转为Handler<T>(通过supports校验,类型匹配)
((Handler<T>) handler).handle(data);
return; // 找到匹配的处理器后立即执行,避免后续遍历
}
}
// 无匹配处理器时抛出异常,便于快速定位问题
throw new UnsupportedOperationException("无支持该类型的处理器:" + clazz.getName());
}
}
示例:
@Service
public class VoucherService {
@Autowired
private HandlerManager handlerManager;
/**
* 发放凭证(统一入口,无需关注具体凭证类型)
* @param voucher 待发放的凭证(泛型父类,如Voucher)
*/
public void issueVoucher(Voucher voucher) {
// 调用处理器工厂,动态匹配并执行逻辑
handlerManager.process(voucher);
}
}
// 调用示例
public class Test {
public static void main(String[] args) {
VoucherService voucherService = new VoucherService(); // 实际开发中通过Spring注入
// 1. 发放电子券(自动匹配EcouponHandler)
Ecoupon ecoupon = new Ecoupon("E20240901", 1L, new Date(System.currentTimeMillis() + 86400000));
voucherService.issueVoucher(ecoupon);
// 2. 发放纸质券(自动匹配PaperVoucherHandler)
PaperVoucher paperVoucher = new PaperVoucher("P2024090101", 1L);
voucherService.issueVoucher(paperVoucher);
}
}
方案优势:
- 完全解耦:业务逻辑(处理器)与类型判断(工厂)分离,新增凭证类型(如红包)时,仅需新增
RedPacketHandler,无需修改HandlerManager或VoucherService; - 类型安全:通过
supports方法校验类型,避免处理器与数据类型不匹配的问题; - 易于维护:每个处理器仅关注一种凭证类型的逻辑,代码结构清晰,排查问题更高效。
场景 4:JSON 泛型反序列化(解决泛型擦除导致的类型丢失问题)
使用
Gson、Jackson等JSON框架反序列化泛型集合(如List<User>、Map<String, Order>)时,直接调用fromJson(json, List.class)会返回List<Object>,需手动强制转换,不仅代码冗余,还存在ClassCastException风险。这是因为Java泛型存在类型擦除机制 —— 编译后泛型信息会被擦除(如List<User>变为List),导致JSON框架无法识别具体类型。
通过TypeReference类保留泛型信息。TypeReference利用匿名子类会保留父类泛型参数的特性,在运行时获取泛型的具体类型,从而让JSON框架正确反序列化。
/**
* 泛型类型引用类:用于保留泛型信息,解决JSON反序列化问题
* @param <T> 泛型类型(如List<User>、Map<String, Order>)
*/
public abstract class TypeReference<T> {
// 存储泛型的具体类型(如List<User>对应的Type对象)
private final Type type;
protected TypeReference() {
// 1. 获取当前匿名子类的父类(即TypeReference<T>)
Type superClass = getClass().getGenericSuperclass();
// 2. 判断父类是否为参数化类型(即是否包含泛型信息)
if (!(superClass instanceof ParameterizedType)) {
throw new IllegalStateException("TypeReference必须使用匿名子类形式创建,如new TypeReference<List<User>>() {}");
}
// 3. 提取泛型的具体类型(getActualTypeArguments()[0]获取第一个泛型参数)
this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
// 对外提供泛型类型,供JSON框架使用
public Type getType() {
returntype;
}
}
封装JSON工具类:
public class JsonUtil {
// 单例Gson对象(避免频繁创建,提升性能)
private static final Gson GSON = new Gson();
/**
* 泛型反序列化:将JSON字符串转为指定泛型类型的对象
* @param json JSON字符串
* @param typeReference 泛型类型引用(如new TypeReference<List<User>>() {})
* @param <T> 目标泛型类型
* @return 反序列化后的对象
*/
public static <T> T fromJson(String json, TypeReference<T> typeReference) {
if (json == null || json.isEmpty()) {
throw new IllegalArgumentException("JSON字符串不能为空");
}
// 调用Gson的fromJson方法,传入保留泛型信息的Type对象
return GSON.fromJson(json, typeReference.getType());
}
// 其他JSON工具方法(如序列化)可在此补充
}
示例:
// 1. 反序列化List<User>
String userJson = "[{\"id\":1,\"name\":\"Alice\",\"age\":25},{\"id\":2,\"name\":\"Bob\",\"age\":28}]";
List<User> userList = JsonUtil.fromJson(userJson, new TypeReference<List<User>>() {});
// 2. 反序列化Map<String, Order>(键为订单ID,值为订单对象)
String orderJson = "{\"1001\":{\"orderId\":1001,\"amount\":99.9},\"1002\":{\"orderId\":1002,\"amount\":199.9}}";
Map<String, Order> orderMap = JsonUtil.fromJson(orderJson, new TypeReference<Map<String, Order>>() {});
// 3. 反序列化泛型结果类Result<List<Voucher>>
String resultJson = "{\"code\":200,\"msg\":\"success\",\"data\":[{\"voucherId\":1,\"code\":\"E20240901\"}]}";
Result<List<Voucher>> voucherResult = JsonUtil.fromJson(resultJson, new TypeReference<Result<List<Voucher>>>() {});
场景 5:泛型工具类:对象拷贝(解决多类型对象转换冗余问题)
业务开发中频繁需要对象转换(如
User(数据库实体)→UserDTO(接口传输对象)、Voucher→VoucherVO(前端展示对象)),若为每个转换场景编写copyUserToDTO、copyVoucherToVO等方法,会导致代码冗余,且转换逻辑分散,维护困难。
定义泛型对象拷贝工具类,通过反射或第三方框架(如 Spring BeanUtils、MapStruct)实现任意类型对象的拷贝,一套逻辑适配所有转换场景。
/**
* 通用对象拷贝工具类
*/
public class ConvertUtil {
/**
* 泛型对象拷贝:将源对象的属性拷贝到目标对象(浅拷贝)
* @param source 源对象(如User)
* @param targetClass 目标对象的Class(如UserDTO.class)
* @param <S> 源对象类型
* @param <T> 目标对象类型
* @return 拷贝后的目标对象
*/
public static <S, T> T copy(S source, Class<T> targetClass) {
// 校验源对象和目标类是否为空
if (source == null) {
throw new IllegalArgumentException("源对象不能为空");
}
if (targetClass == null) {
throw new IllegalArgumentException("目标类不能为空");
}
try {
// 1. 创建目标对象实例(通过无参构造方法)
T target = targetClass.getDeclaredConstructor().newInstance();
// 2. 拷贝属性:Spring BeanUtils支持同名同类型属性的拷贝
BeanUtils.copyProperties(source, target);
return target;
} catch (Exception e) {
// 3. 异常封装:明确异常原因,便于排查问题
throw new RuntimeException("对象拷贝失败:源类型=" + source.getClass().getName() + ",目标类型=" + targetClass.getName(), e);
}
}
/**
* 泛型集合拷贝:将源集合中的每个元素拷贝到目标类型,返回目标类型的集合
* @param sourceList 源集合(如List<User>)
* @param targetClass 目标元素类型(如UserDTO.class)
* @param <S> 源元素类型
* @param <T> 目标元素类型
* @return 目标类型的集合(如List<UserDTO>)
*/
public static <S, T> List<T> copyList(List<S> sourceList, Class<T> targetClass) {
if (sourceList == null || sourceList.isEmpty()) {
return Collections.emptyList(); // 返回空集合,避免NullPointerException
}
// 遍历源集合,逐个拷贝元素到目标类型
return sourceList.stream()
.map(source -> copy(source, targetClass))
.collect(Collectors.toList());
}
}
示例:
// 1. 单个对象拷贝:User → UserDTO
User user = new User(1L, "yian", 25, "yian@example.com");
UserDTO userDTO = ConvertUtil.copy(user, UserDTO.class);
// 2. 集合拷贝:List<Voucher> → List<VoucherVO>
List<Voucher> voucherList = voucherService.list();
List<VoucherVO> voucherVOList = ConvertUtil.copyList(voucherList, VoucherVO.class);
// 3. 复杂场景:PageResult<Voucher> → PageResult<VoucherVO>
PageResult<Voucher> voucherPage = voucherService.page(1, 10);
List<VoucherVO> voucherVOPageList = ConvertUtil.copyList(voucherPage.getList(), VoucherVO.class);
PageResult<VoucherVO> voucherVOPage = new PageResult<>(voucherPage.getTotal(), voucherVOPageList, voucherPage.getPageNum(), voucherPage.getPageSize());
Spring BeanUtils基于反射实现,性能较低。若需处理大量对象拷贝(如批量接口),建议替换为MapStruct(编译期生成拷贝代码,性能接近手写),但泛型封装逻辑可保持不变(只需将BeanUtils.copyProperties替换为MapStruct的映射方法);
高级技巧
泛型通配符(
?)是泛型的进阶特性,很多开发者因不理解其设计意图而滥用,导致代码出现不必要的限制或安全隐患。实际上,通配符的核心是控制泛型类型的范围,根据数据读取或数据写入的场景,可分为上界通配符和下界通配符,两者遵循明确的读写规则。
上界通配符:<? extends T>(能读不能写)
- 语法含义:表示
T及其子类,即泛型类型只能是T或T的子类(如<? extends Number>表示Number、Integer、Double等); - 核心场景:仅需从集合中读取数据,无需写入数据;
- 读写规则:
可读:可以安全地将集合中的元素转为T类型(因为所有元素都是T的子类,向上转型安全);
不可写:不能向集合中添加任何非null元素(因为无法确定集合的具体类型,如List<? extends Number>可能是List<Integer>或List<Double>,添加Integer到List<Double>会导致类型错误)。
示例:
/**
* 计算数字集合的总和(仅读取数据,不写入)
* @param numbers 数字集合(支持Integer、Double等Number子类)
* @return 总和
*/
public double sum(List<? extends Number> numbers) {
double total = 0.0;
for (Number num : numbers) {
// 读取数据:安全,所有元素都是Number的子类,可转为Number类型
total += num.doubleValue();
}
return total;
}
// 使用示例
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sum(intList)); // 输出6.0
System.out.println(sum(doubleList)); // 输出6.6
// 写入限制(编译报错)
numbers.add(4); // ❌ 错误:无法确定集合是List<Integer>还是List<Double>
numbers.add(new Integer(4)); // ❌ 错误:同上,可能与集合实际类型冲突
numbers.add(null); // ✅ 允许:null是所有类型的默认值,不会导致类型错误(但实际开发中很少用)
下界通配符:<? super T>(能写不能读)
- 语法含义:表示
T及其父类,即泛型类型只能是T或T的父类(如<? super Integer>表示Integer、Number、Object等); - 核心场景:仅需向集合中写入数据,无需读取数据(或仅需读取为
Object类型); - 读写规则:
可写:可以安全地向集合中添加T类型的元素(因为T是集合类型的子类,向下转型安全,如List<? super Integer>可以是List<Number>,添加Integer到List<Number>安全);
可读:只能将集合中的元素转为Object类型(因为无法确定集合的具体类型,如List<? super Integer>可能是List<Number>或List<Object>,无法确定元素的具体类型)。
示例:
/**
* 向集合中添加整数(仅写入数据,不读取)
* @param list 整数或其父类的集合(如List<Integer>、List<Number>、List<Object>)
*/
public void addIntegers(List<? super Integer> list) {
// 写入数据:安全,Integer是集合类型的子类
list.add(100);
list.add(200);
// 读取限制
Object obj = list.get(0); // ✅ 允许:只能转为Object类型
Integer num = list.get(0); // ❌ 错误:无法确定集合是List<Number>还是List<Object>
}
// 使用示例
List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
addIntegers(numberList); // 向List<Number>添加Integer,安全
addIntegers(objectList); // 向List<Object>添加Integer,安全
System.out.println(numberList); // 输出[100, 200]
System.out.println(objectList); // 输出[100, 200]更多推荐



所有评论(0)