Java泛型高級玩法之通配符、上下界與類型擦除避坑實(shí)戰(zhàn)
引言
做Java開發(fā)的朋友應(yīng)該都有體會:泛型這東西入門容易,真要用到項目里——比如寫個通用工具類、處理各種類型的集合時,動不動就踩坑。要么通配符用錯編譯報錯,要么類型擦除導(dǎo)致運(yùn)行時拋ClassCastException,要么想創(chuàng)建個泛型數(shù)組直接編譯不過。
今天這篇文章,我不跟大家聊純概念,就結(jié)合我實(shí)際開發(fā)中踩過的坑、寫過的通用數(shù)據(jù)校驗(yàn)工具類,把泛型通配符(?、? extends T、? super T)的用法、類型擦除的坑,還有避坑的實(shí)操技巧,都講清楚。保證都是實(shí)戰(zhàn)干貨,代碼能直接跑,看完就能用到自己的項目里。
一、泛型通配符:3種寫法的實(shí)戰(zhàn)區(qū)別(PECS原則真的很好用)
泛型通配符的核心就是解決“不同類型集合怎么通用處理”的問題,很多人搞不懂 ?、? extends T、? super T的區(qū)別,其實(shí)記住PECS原則(生產(chǎn)者用Extends,消費(fèi)者用Super)就夠了。我結(jié)合自己寫過的工具方法,給大家講透每個通配符的用法和坑。
1. 無界通配符 ?:只做通用讀取,別想著寫數(shù)據(jù)
無界通配符?就是匹配任意類型的泛型,我一般只用在“只讀取不寫入”的通用工具里,比如打印任意集合、判斷集合是否為空。
我當(dāng)初踩過一個坑:用?定義的List,想往里面加個字符串,結(jié)果直接編譯報錯。后來才明白,編譯器根本不知道這個List具體存的是啥類型,為了保證類型安全,除了null,啥都不讓加。
實(shí)戰(zhàn)場景:通用集合打印工具
需求很簡單:寫個方法,不管傳過來的是List、List還是List<自定義對象>,都能打印里面的元素。
import java.util.List;
/**
* 無界通配符實(shí)戰(zhàn):通用集合打印工具
* 快速打印集合,不用重復(fù)寫循環(huán)
*/
public class WildcardDemo {
// 無界通配符?,匹配任意類型的List
public static void printList(List<?> list) {
if (list == null || list.isEmpty()) {
System.out.println("集合為空");
return;
}
for (Object obj : list) {
// 只能讀成Object類型,因?yàn)椴恢谰唧w是啥類型
System.out.println("元素值:" + obj);
}
// 這里是我踩過的坑:想加個字符串,編譯直接報錯
// list.add("test"); // 編譯錯誤:沒法確定list的具體類型,寫入會亂套
// 唯一能加的只有null,因?yàn)閚ull是所有類型的子類
list.add(null);
}
public static void main(String[] args) {
List<String> strList = List.of("Java", "泛型", "通配符");
List<Integer> intList = List.of(1, 2, 3);
printList(strList); // 正常打印字符串列表
printList(intList); // 正常打印整數(shù)列表
}
}
為啥這么寫?
無界通配符的核心就是“通用只讀”,比如打印、統(tǒng)計長度、判空這些操作,不用關(guān)心集合里具體存的是啥,用?就夠了。但千萬別想著往里面寫數(shù)據(jù),除了null,寫啥都報錯。
2. 上界通配符? extends T:只讀取,不寫入(生產(chǎn)者場景)
? extends T的意思是“匹配T或者T的子類”,我一般用在“從集合里讀數(shù)據(jù)”的場景,比如給Integer、Long、Double這些數(shù)值類型的集合求和——這些集合都是“生產(chǎn)”數(shù)值的,所以用extends。
我之前犯過一個錯:用? extends Number的List,想往里加個Integer,結(jié)果編譯報錯?,F(xiàn)在想通了,編譯器不知道這個List是存Integer、Long還是Double,要是加了Integer,萬一原本是Long的List,不就亂套了?
實(shí)戰(zhàn)場景:數(shù)值集合求和工具
需求:寫個方法,能給Integer、Long、Double的List求和,返回double類型結(jié)果。
import java.util.List;
/**
* 上界通配符實(shí)戰(zhàn):數(shù)值集合求和
* 做報表統(tǒng)計時經(jīng)常用這個方法,不用給每種數(shù)值類型都寫一遍求和
*/
public class UpperBoundDemo {
// 上界通配符:匹配Number或其子類(Integer、Long、Double都算)
public static double sum(List<? extends Number> numberList) {
double total = 0.0;
for (Number num : numberList) {
// 讀數(shù)據(jù):所有子類都能轉(zhuǎn)成Number,安全
total += num.doubleValue();
}
// 踩坑點(diǎn):想加Integer,編譯報錯
// numberList.add(10); // 編譯器不知道list是存Integer還是Long,不敢讓加
// 就算加Number也不行
// numberList.add(10.0); // 同樣報錯
return total;
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.5, 2.5);
System.out.println("整數(shù)列表求和:" + sum(intList)); // 輸出6.0
System.out.println("小數(shù)列表求和:" + sum(doubleList)); // 輸出4.0
}
}
核心要點(diǎn):? extends T就是“生產(chǎn)者”——集合里的元素都是T的子類,能安全讀成T類型,但絕對不能寫。比如求和、導(dǎo)出數(shù)據(jù)、遍歷取值這些場景,用這個通配符準(zhǔn)沒錯。
3. 下界通配符? super T:只寫入,讀取只能拿Object(消費(fèi)者場景)
? super T的意思是“匹配T或者T的父類”,我一般用在“往集合里寫數(shù)據(jù)”的場景,比如批量往List里插Integer——不管這個List是List、List還是List,都能插,因?yàn)檫@些都是Integer的父類。
這里也有個坑:用? super T的List讀數(shù)據(jù)時,只能讀成Object類型,想轉(zhuǎn)成T類型會報錯。因?yàn)榫幾g器不知道這個List是存T的哪個父類,沒法確定類型。
實(shí)戰(zhàn)場景:數(shù)據(jù)批量插入工具
需求:寫個方法,能往任意“能存Integer”的集合里批量插整數(shù)。
import java.util.ArrayList;
import java.util.List;
/**
* 下界通配符實(shí)戰(zhàn):數(shù)據(jù)批量插入
* 用這個方法往不同類型的集合里插數(shù)據(jù)
*/
public class LowerBoundDemo {
// 下界通配符:匹配Integer或其父類(Number、Object)
public static void batchAddInteger(List<? super Integer> list, int count) {
for (int i = 1; i <= count; i++) {
// 寫數(shù)據(jù):Integer能存到任意父類集合里,安全
list.add(i);
}
// 讀數(shù)據(jù):只能拿到Object類型,這是坑點(diǎn)
for (Object obj : list) {
System.out.println("插入的元素:" + obj);
}
// 我踩過的坑:想直接讀成Integer,編譯報錯
// for (Integer num : list) { // 編譯錯誤:list可能是Number或Object類型,沒法轉(zhuǎn)Integer
// System.out.println(num);
// }
}
public static void main(String[] args) {
// 場景1:List<Integer>(直接存Integer)
List<Integer> intList = new ArrayList<>();
batchAddInteger(intList, 3); // 插入1、2、3
// 場景2:List<Number>(Integer的父類)
List<Number> numList = new ArrayList<>();
batchAddInteger(numList, 2); // 插入1、2
// 場景3:List<Object>(Integer的頂級父類)
List<Object> objList = new ArrayList<>();
batchAddInteger(objList, 1); // 插入1
}
}
核心要點(diǎn):? super T就是“消費(fèi)者”——集合能接收T類型的數(shù)據(jù),所以寫數(shù)據(jù)絕對安全,但讀數(shù)據(jù)只能拿Object。比如批量插入、批量賦值、數(shù)據(jù)入庫這些場景,就用這個通配符。
4. PECS原則速查表
| 通配符類型 | 能干嘛 | 我常用的場景 | 讀寫注意點(diǎn) |
|---|---|---|---|
| ? | 匹配任意類型 | 打印、判空、統(tǒng)計長度 | 讀:只能拿Object;寫:只能加null |
| ? extends T | 匹配T或其子類 | 求和、導(dǎo)出數(shù)據(jù)、遍歷取值 | 讀:能轉(zhuǎn)T;寫:啥都不能加(除了null) |
| ? super T | 匹配T或其父類 | 批量插入、批量賦值、數(shù)據(jù)入庫 | 讀:只能拿Object;寫:能加T類型 |
二、類型擦除:泛型最坑的地方
很多人不知道,泛型其實(shí)是Java的“語法糖”——編譯的時候,編譯器會把所有泛型信息都擦除掉,換成Object或者限定類型。這就導(dǎo)致了一堆坑,我給大家列幾個我踩過的高頻坑,一個個說怎么解決。
1. 先搞懂類型擦除是啥
編譯的時候,編譯器會把泛型代碼改成“原始代碼”:
- List會被擦成List,讀數(shù)據(jù)時自動加(String)強(qiáng)制轉(zhuǎn)換;
- List<? extends Number>會被擦成List;
- 泛型類、泛型方法里的都會被擦成Object(有上限就擦成上限類型)。
舉個例子,咱們寫的泛型代碼:
// 編譯前
List<String> strList = new ArrayList<>();
strList.add("Java");
String str = strList.get(0);
// 編譯后(編譯器自動改的)
List strList = new ArrayList();
strList.add("Java");
String str = (String) strList.get(0); // 自動加了強(qiáng)制轉(zhuǎn)換
看出來了吧?泛型只在編譯期管類型安全,運(yùn)行時JVM根本不知道泛型是啥,這就是所有坑的根源。
2. 我踩過的5個高頻坑(附解決方案)
坑1:泛型數(shù)組創(chuàng)建失?。ㄗ畛R姷目樱?/h4>
我當(dāng)初想創(chuàng)建一個List[]數(shù)組,結(jié)果編譯直接報錯;后來想強(qiáng)行轉(zhuǎn)類型,運(yùn)行時又拋ClassCastException。
踩坑代碼:
/**
* 我踩過的坑:泛型數(shù)組創(chuàng)建失敗
*/
public class TypeErasureDemo1 {
public static void main(String[] args) {
// 坑1:直接創(chuàng)建泛型數(shù)組,編譯報錯
// List<String>[] strArr = new List<String>[10]; // 編譯錯誤:Generic array creation
// 坑2:強(qiáng)行轉(zhuǎn)類型,運(yùn)行時報錯
List<String>[] strArr2 = (List<String>[]) new List[10];
strArr2[0] = new ArrayList<String>();
// 運(yùn)行時坑:數(shù)組實(shí)際是List[],能存任意List
List<Integer> intList = new ArrayList<>();
intList.add(123);
strArr2[0] = (List<String>) intList; // 編譯過了,運(yùn)行時拋ClassCastException
}
}
為什么?
數(shù)組是“協(xié)變”的(比如String[]是Object[]的子類),但泛型是“不變”的(List不是List的子類)。類型擦除后,JVM分不清List[]和List[],所以編譯器直接不讓創(chuàng)建泛型數(shù)組。
我的解決方案:別用數(shù)組,改用List<List>!這是我現(xiàn)在最常用的辦法,簡單又安全。
/**
* 我的解決方案:用List代替泛型數(shù)組
*/
public class TypeErasureDemo1Fix {
public static void main(String[] args) {
// 用List<List<String>>代替List<String>[]
List<List<String>> strListContainer = new ArrayList<>();
List<String> strList = new ArrayList<>();
strList.add("Java");
strListContainer.add(strList);
// 類型安全:想存List<Integer>直接編譯報錯,不會有運(yùn)行時問題
// List<Integer> intList = List.of(1);
// strListContainer.add(intList); // 編譯錯誤,類型對不上
}
}
坑2:用instanceof判斷泛型類型
我當(dāng)初想判斷一個List是不是List,寫了instanceof List,結(jié)果編譯報錯。
踩坑代碼:
/**
* 我踩過的坑:instanceof判斷泛型類型
*/
public class TypeErasureDemo2 {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
// 坑:instanceof沒法判斷泛型類型,編譯報錯
// if (strList instanceof List<String>) { // 編譯錯誤
// 看似能寫,但沒意義——只能判斷是不是List,沒法判斷泛型
if (strList instanceof List<?>) {
System.out.println("是List,但不知道存的是啥");
}
}
}
為什么?
運(yùn)行時泛型信息已經(jīng)被擦除了,JVM只知道是List,不知道是List還是List,所以instanceof根本判斷不了。
我的解決方案:傳個Class類型令牌,遍歷集合判斷每個元素的類型。
import java.util.List;
/**
* 我的解決方案:用類型令牌判斷元素類型
*/
public class TypeErasureDemo2Fix {
// 我項目里的通用方法:判斷List里的元素是不是指定類型
public static <T> boolean isListOfType(List<?> list, Class<T> type) {
if (list.isEmpty()) {
return false; // 空集合沒法判斷
}
for (Object obj : list) {
if (!type.isInstance(obj)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
List<String> strList = List.of("Java", "泛型");
List<Integer> intList = List.of(1, 2);
// 用類型令牌判斷,靠譜!
System.out.println("是不是String列表:" + isListOfType(strList, String.class)); // true
System.out.println("是不是String列表:" + isListOfType(intList, String.class)); // false
}
}
坑3:泛型類里定義靜態(tài)泛型變量
我當(dāng)初在泛型類里寫了個static T staticValue,結(jié)果編譯直接報錯,后來才知道靜態(tài)變量和泛型實(shí)例沒關(guān)系。
踩坑代碼:
/**
* 我踩過的坑:泛型類里的靜態(tài)變量不能用泛型
*/
public class TypeErasureDemo3<T> {
// 坑:靜態(tài)變量用T,編譯報錯
// private static T staticValue; // Compile error: Cannot make a static reference to the non-static type T
// 勉強(qiáng)能寫,但沒意義
private static List<?> staticList = new ArrayList<>();
}
為什么?
靜態(tài)變量屬于“類”,不是屬于“實(shí)例”。比如我創(chuàng)建TypeErasureDemo3和TypeErasureDemo3,這兩個實(shí)例共享同一個靜態(tài)變量,編譯器沒法給靜態(tài)變量綁定具體的T類型。
我的解決方案:靜態(tài)方法單獨(dú)定義泛型參數(shù),別用類的泛型參數(shù);靜態(tài)變量要么用具體類型,要么用?。
import java.util.ArrayList;
import java.util.List;
/**
* 我的解決方案:靜態(tài)方法自己定義泛型參數(shù)
*/
public class TypeErasureDemo3Fix {
// 靜態(tài)變量:用具體類型,別用泛型
private static List<String> staticStrList = new ArrayList<>();
// 靜態(tài)泛型方法:自己定義<T>,和類無關(guān)
public static <T> void addElement(List<T> list, T element) {
list.add(element);
}
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 靜態(tài)方法能支持不同類型,好用!
addElement(strList, "Java");
addElement(intList, 123);
System.out.println(strList); // [Java]
System.out.println(intList); // [123]
}
}
坑4:try-catch捕獲泛型異常
我當(dāng)初想自定義一個泛型異常,然后用catch捕獲GenericException,結(jié)果編譯報錯。
踩坑代碼:
/**
* 我踩過的坑:捕獲泛型異常
*/
public class TypeErasureDemo4 {
// 自定義泛型異常(編譯不報錯,但用不了)
static class GenericException<T> extends Exception {
private T errorData;
public GenericException(T errorData) {
this.errorData = errorData;
}
}
public static void main(String[] args) {
try {
throw new GenericException<>("測試異常");
} catch (GenericException<String> e) { // 編譯錯誤:不讓捕泛型異常
e.printStackTrace();
}
}
}
為什么?
異常處理是運(yùn)行時的事,類型擦除后,GenericException和GenericException都變成了GenericException,JVM分不清,所以編譯器直接不讓捕。
我的解決方案:別定義泛型異常,改用Object存錯誤數(shù)據(jù),然后寫個泛型方法取數(shù)據(jù)。
/**
* 我的解決方案:非泛型異常+泛型getter
*/
public class TypeErasureDemo4Fix {
// 非泛型異常,用Object存任意類型數(shù)據(jù)
static class DataException extends Exception {
private Object errorData;
public DataException(Object errorData) {
this.errorData = errorData;
}
// 泛型方法:安全取數(shù)據(jù),自己控制類型轉(zhuǎn)換
public <T> T getErrorData(Class<T> type) {
if (type.isInstance(errorData)) {
return type.cast(errorData);
}
throw new ClassCastException("類型對不上");
}
}
public static void main(String[] args) {
try {
throw new DataException("字符串錯誤");
// throw new DataException(12345); // 也能傳整數(shù)
} catch (DataException e) {
// 安全取數(shù)據(jù),不會亂轉(zhuǎn)
String errorMsg = e.getErrorData(String.class);
System.out.println("錯誤信息:" + errorMsg);
}
}
}
坑5:泛型方法重載
我當(dāng)初寫了兩個processData方法,一個接List,一個接List,結(jié)果編譯報錯,說方法簽名重復(fù)。
踩坑代碼:
/**
* 我踩過的坑:泛型方法重載沖突
*/
public class TypeErasureDemo5 {
// 方法1:處理String列表
public static void processData(List<String> list) {
System.out.println("處理字符串");
}
// 方法2:處理Integer列表,編譯報錯
// public static void processData(List<Integer> list) { // 編譯錯誤:簽名擦除后一樣
// System.out.println("處理整數(shù)");
// }
}
為什么?
類型擦除后,List和List都變成了List,兩個方法的簽名都是processData(List),編譯器分不清,所以不讓重載。
我的解決方案:要么加個Class參數(shù)區(qū)分簽名,要么直接改方法名。我一般加類型令牌,不用改方法名。
import java.util.List;
/**
* 我的解決方案:加類型令牌區(qū)分重載
*/
public class TypeErasureDemo5Fix {
// 通用方法:加類型令牌,判斷類型后處理
public static <T> void processData(List<T> list, Class<T> type) {
if (type == String.class) {
System.out.println("處理字符串列表:" + list);
} else if (type == Integer.class) {
System.out.println("處理整數(shù)列表:" + list);
} else {
System.out.println("處理其他類型");
}
}
public static void main(String[] args) {
List<String> strList = List.of("Java", "泛型");
List<Integer> intList = List.of(1, 2);
// 傳類型令牌,就能區(qū)分了
processData(strList, String.class);
processData(intList, Integer.class);
}
}
三、實(shí)戰(zhàn)案例:通用數(shù)據(jù)校驗(yàn)工具類
光說不練假把式,我把上面的知識點(diǎn)都揉進(jìn)一個“通用數(shù)據(jù)校驗(yàn)工具類”里,這是我實(shí)際項目里用來校驗(yàn)各種數(shù)據(jù)的,能避開前面說的所有坑,大家可以直接拿去改改用。
1. 需求說明
我做的這個工具類,要實(shí)現(xiàn)這幾個功能:
- 校驗(yàn)任意類型的List里的元素是否符合規(guī)則(比如字符串非空、數(shù)值大于0);
- 批量把符合規(guī)則的數(shù)據(jù)寫到另一個集合里;
- 能獲取校驗(yàn)失敗的數(shù)據(jù),還能拋出帶錯誤數(shù)據(jù)的異常;
- 避開泛型的各種坑,保證類型安全。
2. 完整代碼
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
/**
* 通用數(shù)據(jù)校驗(yàn)工具類
* 功能:校驗(yàn)任意類型數(shù)據(jù)、批量寫入有效數(shù)據(jù)、返回?zé)o效數(shù)據(jù)、拋出帶錯誤信息的異常
* 避坑點(diǎn):通配符用法、類型擦除、靜態(tài)泛型、異常處理
*/
public class GenericDataValidator<T> {
// 靜態(tài)常量:不用泛型,避免靜態(tài)泛型坑
private static final String DEFAULT_ERROR_MSG = "數(shù)據(jù)校驗(yàn)失敗";
// 類型令牌:解決類型擦除后沒法判斷類型的問題
private final Class<T> typeToken;
// 構(gòu)造方法:傳入類型令牌,必須的!
public GenericDataValidator(Class<T> typeToken) {
this.typeToken = typeToken;
}
/**
* 校驗(yàn)所有元素是否符合規(guī)則(上界通配符:只讀取,生產(chǎn)者)
* @param dataList 待校驗(yàn)集合(T或T的子類)
* @param rule 校驗(yàn)規(guī)則(比如字符串非空、數(shù)值大于0)
* @return true=全部符合,false=有不符合的
*/
public boolean validateAll(List<? extends T> dataList, Predicate<T> rule) {
if (dataList == null || dataList.isEmpty()) {
return true;
}
for (T data : dataList) {
// 先判斷類型對不對,避免類型擦除導(dǎo)致的轉(zhuǎn)換異常
if (!typeToken.isInstance(data)) {
throw new ClassCastException("集合里有不是" + typeToken.getName() + "類型的數(shù)據(jù)");
}
// 再校驗(yàn)規(guī)則
if (!rule.test(data)) {
return false;
}
}
return true;
}
/**
* 批量寫入有效數(shù)據(jù)(下界通配符:只寫入,消費(fèi)者)
* @param targetList 目標(biāo)集合(T或T的父類)
* @param sourceList 源數(shù)據(jù)集合
* @param rule 校驗(yàn)規(guī)則
* @return 成功寫入的數(shù)量
*/
public int addValidData(List<? super T> targetList, List<? extends T> sourceList, Predicate<T> rule) {
// 先判空,避免空指針
Objects.requireNonNull(targetList, "目標(biāo)集合不能為null");
Objects.requireNonNull(sourceList, "源數(shù)據(jù)集合不能為null");
int successCount = 0;
for (T data : sourceList) {
if (rule.test(data)) {
targetList.add(data); // 下界通配符,寫入安全
successCount++;
}
}
return successCount;
}
/**
* 獲取校驗(yàn)失敗的數(shù)據(jù)(靜態(tài)泛型方法:自己定義<T>,避開靜態(tài)泛型坑)
* @param dataList 待校驗(yàn)集合
* @param rule 校驗(yàn)規(guī)則
* @param <T> 數(shù)據(jù)類型
* @return 失敗的數(shù)據(jù)列表
*/
public static <T> List<T> getInvalidData(List<? extends T> dataList, Predicate<T> rule) {
List<T> invalidList = new ArrayList<>();
if (dataList == null || dataList.isEmpty()) {
return invalidList;
}
for (T data : dataList) {
if (!rule.test(data)) {
invalidList.add(data);
}
}
return invalidList;
}
/**
* 自定義異常:非泛型,用Object存錯誤數(shù)據(jù)(避開泛型異??樱?
*/
public static class ValidationException extends RuntimeException {
private final Object invalidData; // 存任意類型的錯誤數(shù)據(jù)
public ValidationException(String message, Object invalidData) {
super(message);
this.invalidData = invalidData;
}
// 泛型方法:安全獲取錯誤數(shù)據(jù)
public <E> E getInvalidData(Class<E> type) {
if (type.isInstance(invalidData)) {
return type.cast(invalidData);
}
throw new ClassCastException("錯誤數(shù)據(jù)類型不對,想要:" + type.getName());
}
}
/**
* 嚴(yán)格校驗(yàn):失敗就拋異常
*/
public void strictValidate(List<? extends T> dataList, Predicate<T> rule) {
List<T> invalidData = getInvalidData(dataList, rule);
if (!invalidData.isEmpty()) {
// 拋非泛型異常,帶錯誤數(shù)據(jù)
throw new ValidationException(DEFAULT_ERROR_MSG, invalidData.get(0));
}
}
// 測試方法
public static void main(String[] args) {
// 1. 字符串校驗(yàn)示例(校驗(yàn)非空非空白)
GenericDataValidator<String> strValidator = new GenericDataValidator<>(String.class);
List<String> strList = List.of("Java", "", "泛型", " ", "高級特性");
// 校驗(yàn)規(guī)則:非空且非空白
Predicate<String> strRule = s -> s != null && !s.isBlank();
// 校驗(yàn)所有數(shù)據(jù)
boolean allValid = strValidator.validateAll(strList, strRule);
System.out.println("字符串是否全部有效:" + allValid); // false
// 獲取無效數(shù)據(jù)
List<String> invalidStr = GenericDataValidator.getInvalidData(strList, strRule);
System.out.println("無效字符串:" + invalidStr); // ["", " "]
// 批量寫入有效數(shù)據(jù)到Object集合(下界通配符)
List<Object> targetList = new ArrayList<>();
int successCount = strValidator.addValidData(targetList, strList, strRule);
System.out.println("成功寫入有效字符串?dāng)?shù)量:" + successCount); // 3
System.out.println("目標(biāo)集合內(nèi)容:" + targetList); // [Java, 泛型, 高級特性]
// 嚴(yán)格校驗(yàn),失敗拋異常
try {
strValidator.strictValidate(strList, strRule);
} catch (ValidationException e) {
String invalidData = e.getInvalidData(String.class);
System.out.println("校驗(yàn)失敗,錯誤數(shù)據(jù):" + invalidData); // ""
}
// 2. 整數(shù)校驗(yàn)示例(校驗(yàn)大于0)
GenericDataValidator<Integer> intValidator = new GenericDataValidator<>(Integer.class);
List<Integer> intList = List.of(10, 20, -5, 30);
Predicate<Integer> intRule = num -> num > 0;
boolean intAllValid = intValidator.validateAll(intList, intRule);
System.out.println("整數(shù)是否全部有效:" + intAllValid); // false
List<Integer> invalidInt = GenericDataValidator.getInvalidData(intList, intRule);
System.out.println("無效整數(shù):" + invalidInt); // [-5]
}
}
3. 這個工具類的亮點(diǎn)
- 通配符用得對:讀取數(shù)據(jù)用? extends T,寫入數(shù)據(jù)用? super T,嚴(yán)格遵守PECS原則,不會有編譯報錯;
- 避開了所有類型擦除的坑:用類型令牌判斷類型、靜態(tài)方法自己定義泛型、非泛型異常存錯誤數(shù)據(jù)、用List代替數(shù)組;
- 類型安全:所有操作都做了類型校驗(yàn),不會出現(xiàn)運(yùn)行時ClassCastException;
- 實(shí)用性強(qiáng):我在項目里校驗(yàn)表單數(shù)據(jù)、批量入庫數(shù)據(jù)時,都是直接用這個工具類,改改校驗(yàn)規(guī)則就行。
四、我的泛型避坑清單
最后給大家整理個避坑清單,都是我踩過的教訓(xùn),記下來能少走很多彎路:
| 坑的場景 | 為啥會坑 | 我咋解決的 |
|---|---|---|
| 泛型數(shù)組創(chuàng)建失敗 | 類型擦除后數(shù)組分不清泛型類型 | 用List<List>代替泛型數(shù)組 |
| instanceof判斷泛型類型 | 運(yùn)行時沒泛型信息 | 傳Class類型令牌,遍歷判斷元素類型 |
| 靜態(tài)變量用泛型 | 靜態(tài)變量屬于類,沒法綁定實(shí)例泛型 | 靜態(tài)方法自己定義泛型參數(shù),靜態(tài)變量用具體類型 |
| catch捕獲泛型異常 | 運(yùn)行時泛型信息被擦除,JVM分不清 | 不用泛型異常,用Object存錯誤數(shù)據(jù),手動轉(zhuǎn)型 |
| 泛型方法重載沖突 | 擦除后方法簽名一樣 | 加Class參數(shù)區(qū)分,或改方法名 |
| 通配符寫入數(shù)據(jù)報錯 | 違反PECS原則 | 讀數(shù)據(jù)用? extends T,寫數(shù)據(jù)用? super T |
五、總結(jié)
泛型這東西,真的不用死記硬背概念,核心就兩點(diǎn):
- 通配符記住PECS原則——生產(chǎn)者(讀數(shù)據(jù))用extends,消費(fèi)者(寫數(shù)據(jù))用super;
- 類型擦除記住“編譯期管類型,運(yùn)行時沒泛型”,遇到坑就用類型令牌、List代替數(shù)組、非泛型異常這些辦法補(bǔ)。
到此這篇關(guān)于Java泛型高級玩法之通配符、上下界與類型擦除避坑實(shí)戰(zhàn)的文章就介紹到這了,更多相關(guān)Java泛型內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java基礎(chǔ)學(xué)習(xí)之構(gòu)造方法詳解
這篇文章主要為大家詳細(xì)介紹了Java基礎(chǔ)學(xué)習(xí)中構(gòu)造方法的概述及注意事項,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Java有一定幫助,需要的可以參考一下2022-08-08
解決springboot整合cxf-jaxrs中json轉(zhuǎn)換的問題
這篇文章主要介紹了解決springboot整合cxf-jaxrs中json轉(zhuǎn)換的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
關(guān)于@SpringBootApplication詳解
這篇文章主要介紹了關(guān)于@SpringBootApplication的使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08
idea2020.3配置maven環(huán)境并配置Tomcat的詳細(xì)教程
這篇文章主要介紹了idea2020.3配置maven環(huán)境并配置Tomcat的詳細(xì)教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03
Java中的stream流的概念解析及實(shí)際運(yùn)用總結(jié)
流是指傳輸時的數(shù)據(jù),Java為流準(zhǔn)備了很多內(nèi)置類,尤其是IO輸入輸出流非常常用,這里我們來看一下Java中的stream流的概念解析及實(shí)際運(yùn)用總結(jié)2016-06-06

