告別重復代碼!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]

























