开发者

Java泛型高级玩法之通配符、上下界与类型擦除避坑实战

开发者 https://www.devze.com 2025-12-07 10:26 出处:网络 作者: 程序员丘山
价值2999元 Java视频教程限时免费下载
专为Java开发者设计,涵盖核心技术、架构设计、性能优化等
立即下载
目录引言一、泛型通配符:3种写法的实战区别(PECS原则真的很好用)1. 无界通配符 ?:只做通用读取,别想着写数据2. 上界通配符? extends T:只读取,不写入(生产者场景)3. 下界通配符? super T:只写入,读取只能
目录
  • 引言
  • 一、泛型通配符:3种写法的实战区别(PECS原则真的很好用)
    • 1. 无界通配符 ?:只做通用读取,别想着写数据
    • 2. 上界通配符? extends T:只读取,不写入(生产者场景)
    • 3. 下界通配符? super T:只写入,读取只能拿Object(消费者场景)
    • 4. PECS原则速查表
  • 二、类型擦除:泛型最坑的地方
    • 1. 先搞懂类型擦除是啥
    • 2. 我踩过的5个高频坑(附解决方案)
      • 坑1:泛型数组创建失败(最常见的坑)
      • 坑2:用instanceof判断泛型类型
      • 坑3:泛型类里定义静态泛型变量
      • 坑4:try-catch捕获泛型异常
      • 坑5:泛型方法重载
  • 三、实战案例:通用数据校验工具类
    • 1. 需求说明
      • 2. 完整代码
        • 3. 这个工具类的亮点
        • 四、我的泛型避坑清单
          • 五、总结

            引言

            做Java开发的朋友应该都有体会:泛型这东西入门容易,真要用到项目里——比如写个通用工具类、处理各种类型的集合时,动不动就踩坑。要么通配符用错编译报错,要么类型擦除导致运行时抛ClassCastException,要么想创建个泛型数组直接编译不过。

            今天这篇文章,我不跟大家聊纯概念,就结合我实际开发中踩过的坑、写过的通用数据校验工具类,把泛型通配符(?、? extends T、? super T)的用法、类型擦除的坑,还有避坑的实操技巧,都讲清楚。保证都是实战干货,代码能直接跑,看完就能用到自己的项目里。

            一、泛型通配符:3种写法的实战区别(PECS原则真的很好用)

            泛型通配符的核心就是解决“不同类型集合怎么通用处理”的问题,很多人搞不懂 ?、? extends T、? super T的区别,其实记住PECS原则(生产者用Extends,消费者用Super)就够了。我结合自己写过的工具方法,给大家讲透每个通配符的用法和坑。

            1. 无界通配符 ?:只做通用读取,别想着写数据

            无界通配符?就是匹配任意类型的泛型,我一般只用在“只读取不写入”的通用工具里,比如打印任意集合、判断集合是否为空。

            我当初踩过一个坑:用?定义的List,想往里面加个字符串,结果直接编译报错。后来才明白,编译器根本不知道这个List具体存的是啥类型,为了保证类型安全,除了null,啥都不让加。

            实战场景:通用集合打印工具

            需求很简单:写个方法,不管传过来的是List、List还是List<自定义对象>,都能打印里面的元素。

            import java.util.List;
            
            /**
             * 无界通配符实战:通用集合打印工具
             * 快速打印集合,不用重复写循环
             */
            public class WildcardDemo {
                // 无界通配符?,匹配任意类型的List
                public static void printList(List<?> list) {
                    if (list == null || list.isEmpty()) {
                        System.out.println("集合为空");
                        return;
                    }
                    for (Object obj : list) {
                        // 只能读成Object类型,因为不知道具体是啥类型
                        System.out.println("元素值:" + obj);
                    }
                    
                    // 这里是我踩过的坑:想加个字符串,编译直接报错
                    // list.add("test"); // 编译错误:没法确定list的具体类型,写入会乱套
                    // 唯一能加的只有null,因为null是所有类型的子类
                    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); // 正常打印整数列表
                }
            }
            

            为啥这么写?

            无界通配符的核心就是“通用只读”,比如打印、统计长度、判空这些操作,不用关心集合里具体存的是啥,用?就够了。但千万别想着往里面写数据,除了null,写啥都报错。

            2. 上界通配符? extends T:只读取,不写入(生产者场景)

            ? extends T的意思是“匹配T或者T的子类”,我一般用在“从集合里读数据”的场景,比如给Integer、Long、Double这些数值类型的集合求和——这些集合都是“生产”数值的,所以用extends

            我之前犯过一个错:用? extends Number的List,想往里加个Integer,结果编译报错。现在想通了,编译器不知道这个List是存Integer、Long还是Double,要是加了Integer,万一原本是Long的List,不就乱套了?

            实战场景:数值集合求和工具

            需求:写个方法,能给Integer、Long、Double的List求和,返回double类型结果。

            import java.util.List;
            
            /**
             * 上界通配符实战:数值集合求和
             * 做报表统计时经常用这个方法,不用给每种数值类型都写一遍求和
             */
            public class UpperBoundDemo {
                // 上界通配符:匹配Number或其子类(Integer、Long、Double都算)
                public static double sum(List<? extends Number> numberList) {
                    double total = 0.0;
                    for (Number num : numberList) {
                        // 读数据:所有子类都能转成Number,安全
                        total += num.doubleValue();
                    }
                    
                    // 踩坑点:想加Intehttp://www.devze.comger,编译报错
                    // numberList.add(10); // 编译器不知道list是存Integer还是Long,不敢让加
                    // 就算加Number也不行
                    // nhttp://www.devze.comumberList.add(10.0); // 同样报错
                    
                    return total;
                }
            
                public static void main(String[] args) {
                    List<Integer> intList = List.of(1, 2, 3);
                    List<Double> doubleList = Lis编程t.of(1.5, 2.5);
                    
                    System.out.println("整数列表求和:" + sum(intList)); // 输出6.0
                    System.out.println("小数列表求和:" + sum(doubleList)); // 输出4.0
                }
            }
            

            核心要点? extends T就是“生产者”——集合里的元素都是T的子类,能安全读成T类型,但绝对不能写。比如求和、导出数据、遍历取值这些场景,用这个通配符准没错。

            3. 下界通配符? super T:只写入,读取只能拿Object(消费者场景)

            ? super T的意思是“匹配T或者T的父类”,我一般用在“往集合里写数据”的场景,比如批量往List里插Integer——不管这个List是List、List还是List,都能插,因为这些都是Integer的父类。

            这里也有个坑:用? super T的List读数据时,只能读成Object类型,想转成T类型会报错。因为编译器不知道这个List是存T的哪个父类,没法确定类型。

            实战场景:数据批量插入工具

            需求:写个方法,能往任意“能存Integer”的集合里批量插整数。

            import java.util.ArrayList;
            import java.util.List;
            
            /**
             * 下界通配符实战:数据批量插入
             * 用这个方法往不同类型的集合里插数据
             */
            public class LowerBoundDemo {
                // 下界通配符:匹配Integer或其父类(Number、Object)
                public static void BATchAddInteger(List<? super Integer> list, int count) {
                    for (int i = 1; i <= count; i++) {
                        // 写数据:Integer能存到任意父类集合里,安全
                        list.add(i);
                    }
                    
                    // 读数据:只能拿到Object类型,这是坑点
                    for (Object obj : list) {
                        System.out.println("插入的元素:" + obj);
                    }
                    
                    // 我踩过的坑:想直接读成Integer,编译报错
                    // for (Integer num : list) { // 编译错误:list可能是Number或Object类型,没法转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<Objecphpt> objList = new ArrayList<>();
                    batchAwww.devze.comddInteger(objList, 1); // 插入1
                }
            }
            

            核心要点? super T就是“消费者”——集合能接收T类型的数据,所以写数据绝对安全,但读数据只能拿Object。比如批量插入、批量赋值、数据入库这些场景,就用这个通配符。

            4. PECS原则速查表

            通配符类型能干嘛我常用的场景读写注意点
            ?匹配任意类型打印、判空、统计长度读:只能拿Object;写:只能加null
            ? extends T匹配T或其子类求和、导出数据、遍历取值读:能转T;写:啥都不能加(除了null)
            ? super T匹配T或其父类批量插入、批量赋值、数据入库读:只能拿Object;写:能加T类型

            二、类型擦除:泛型最坑的地方

            很多人不知道,泛型其实是Java的“语法糖”——编译的时候,编译器会把所有泛型信息都擦除掉,换成Object或者限定类型。这就导致了一堆坑,我给大家列几个我踩过的高频坑,一个个说怎么解决。

            1. 先搞懂类型擦除是啥

            编译的时候,编译器会把泛型代码改成“原始代码”:

            • List会被擦成List,读数据时自动加(String)强制转换;
            • 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); // 自动加了强制转换
            

            看出来了吧?泛型只在编译期管类型安全,运行时JVM根本不知道泛型是啥,这就是所有坑的根源。

            2. 我踩过的5个高频坑(附解决方案)

            坑1:泛型数组创建失败(最常见的坑)

            我当初想创建一个List[]数组,结果编译直接报错;后来想强行转类型,运行时又抛ClassCastException。

            踩坑代码

            /**
             * 我踩过的坑:泛型数组创建失败
             */
            public class TypeErasureDemo1 {
                public static void main(String[] args) {
                    // 坑1:直接创建泛型数组,编译报错
                    // List<String>[] strArr = new List<String>[10]; // 编译错误:Generic array creation
                    
                    // 坑2:强行转类型,运行时报错
                    List<String>[] strArr2 = (List<String>[]) new List[10];
                    strArr2[0] = new ArrayList<String>();
                    // 运行时坑:数组实际是List[],能存任意List
                    List<Integer> intList = new ArrayList<>();
                    intList.add(123);
                    strArr2[0] = (List<String>) intList; // 编译过了,运行时抛ClassCastException
                }
            }
            

            为什么?

            数组是“协变”的(比如String[]是Object[]的子类),但泛型是“不变”的(List不是List的子类)。类型擦除后,JVM分不清List[]和List[],所以编译器直接不让创建泛型数组。

            我的解决方案:别用数组,改用List<List>!这是我现在最常用的办法,简单又安全。

            /**
             * 我的解决方案:用List代替泛型数组
             */
            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>直接编译报错,不会有运行时问题
                    // List<Integer> intList = List.of(1);
                    // strListContainer.add(intList); // 编译错误,类型对不上
                }
            }
            

            坑2:用instanceof判断泛型类型

            我当初想判断一个List是不是List,写了instanceof List,结果编译报错。

            踩坑代码

            /**
             * 我踩过的坑: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,但不知道存的是啥");
                    }
                }
            }
            

            为什么?

            运行时泛型信息已经被擦除了,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:泛型类里定义静态泛型变量

            我当初在泛型类里写了个static T staticValue,结果编译直接报错,后来才知道静态变量和泛型实例没关系。

            踩坑代码

            /**
             * 我踩过的坑:泛型类里的静态变量不能用泛型
             */
            public class TypeErasureDemo3<T> {
                // 坑:静态变量用T,编译报错
                // private static T staticValue; // Compile error: Cannot make a static reference to the non-static type T
                
                // 勉强能写,但没意义
                private static List<?> staticList = new ArrayList<>();
            }
            

            为什么?

            静态变量属于“类”,不是属于“实例”。比如我创建TypeErasureDemo3和TypeErasureDemo3,这两个实例共享同一个静态变量,编译器没法给静态变量绑定具体的T类型。

            我的解决方案:静态方法单独定义泛型参数,别用类的泛型参数;静态变量要么用具体类型,要么用?。

            import java.util.ArrayList;
            import java.util.List;
            
            /**
             * 我的解决方案:静态方法自己定义泛型参数
             */
            public class TypeErasureDemo3Fix {
                // 静态变量:用具体类型,别用泛型
                private static List<String> staticStrList = new ArrayList<>();
            
                // 静态泛型方法:自己定义<T>,和类无关
                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<>();
                    
                    // 静态方法能支持不同类型,好用!
                    addElement(strList, "Java");
                    addElement(intList, 123);
                    
                    System.out.println(strList); // [Java]
                    System.out.println(intList); // [123]
                }
            }
            

            坑4:try-catch捕获泛型异常

            我当初想自定义一个泛型异常,然后用catch捕获GenericException,结果编译报错。

            踩坑代码

            /**
             * 我踩过的坑:捕获泛型异常
             */
            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();
                    }
                }
            }
            

            为什么?

            异常处理是运行时的事,类型擦除后,GenericException和GenericException都变成了GenericException,JVM分不清,所以编译器直接不让捕。

            我的解决方案:别定义泛型异常,改用Object存错误数据,然后写个泛型方法取数据。

            /**
             * 我的解决方案:非泛型异常+泛型getter
             */
            public class TypeErasureDemo4Fix {
                // 非泛型异常,用Object存任意类型数据
                static class DataException extends Exception {
                    private Object errorData;
                    
                    public DataException(Object errorData) {
                        this.errorData = errorData;
                    }
                    
                    // 泛型方法:安全取数据,自己控制类型转换
                    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); // 也能传整数
                    } catch (DataException e) {
                        // 安全取数据,不会乱转
                        String errorMsg = e.getErrorData(String.class);
                        System.out.println("错误信息:" + errorMsg);
                    }
                }
            }
            

            坑5:泛型方法重载

            我当初写了两个processData方法,一个接List,一个接List,结果编译报错,说方法签名重复。

            踩坑代码

            /**
             * 我踩过的坑:泛型方法重载冲突
             */
            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("处理整数");
                // }
            }
            

            为什么?

            类型擦除后,List和List都变成了List,两个方法的签名都是processData(List),编译器分不清,所以不让重载。

            我的解决方案:要么加个Class参数区分签名,要么直接改方法名。我一般加类型令牌,不用改方法名。

            import java.util.List;
            
            /**
             * 我的解决方案:加类型令牌区分重载
             */
            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("处理整数列表:" + list);
                    } else {
                        System.out.println("处理其他类型");
                    }
                }
            
                public static void main(String[] args) {
                    List<String> strList = List.of("Java", "泛型");
                    List<Integer> intList = List.of(1, 2);
                    
                    // 传类型令牌,就能区分了
                    processData(strList, String.class);
                    processData(intList, Integer.class);
                }
            }
            

            三、实战案例:通用数据校验工具类

            光说不练假把式,我把上面的知识点都揉进一个“通用数据校验工具类”里,这是我实际项目里用来校验各种数据的,能避开前面说的所有坑,大家可以直接拿去改改用。

            1. 需求说明

            我做的这个工具类,要实现这几个功能:

            • 校验任意类型的List里的元素是否符合规则(比如字符串非空、数值大于0);
            • 批量把符合规则的数据写到另一个集合里;
            • 能获取校验失败的数据,还能抛出带错误数据的异常;
            • 避开泛型的各种坑,保证类型安全。

            2. 完整代码

            import java.util.ArrayList;
            import java.util.List;
            import java.util.Objects;
            import java.util.function.Predicate;
            
            /**
             * 通用数据校验工具类
             * 功能:校验任意类型数据、批量写入有效数据、返回无效数据、抛出带错误信息的异常
             * 避坑点:通配符用法、类型擦除、静态泛型、异常处理
             */
            public class GenericDataValidator<T> {
                // 静态常量:不用泛型,避免静态泛型坑
                private static final String DEFAULT_ERROR_MSG = "数据校验失败";
                // 类型令牌:解决类型擦除后没法判断类型的问题
                private final Class<T> typeToken;
            
                // 构造方法:传入类型令牌,必须的!
                public GenericDataValidator(Class<T> typeToken) {
                    this.typeToken = typeToken;
                }
            
                /**
                 * 校验所有元素是否符合规则(上界通配符:只读取,生产者)
                 * @param dataList 待校验集合(T或T的子类)
                 * @param rule 校验规则(比如字符串非空、数值大于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) {
                        // 先判断类型对不对,避免类型擦除导致的转换异常
                        if (!typeToken.isInstance(data)) {
                            throw new ClassCastException("集合里有不是" + typeToken.getName() + "类型的数据");
                        }
                        // 再校验规则
                        if (!rule.test(data)) {
                            return false;
                        }
                    }
                    return true;
                }
            
                /**
                 * 批量写入有效数据(下界通配符:只写入,消费者)
                 * @param targetList 目标集合(T或T的父类)
                 * @param sourceList 源数据集合
                 * @param rule 校验规则
                 * @return 成功写入的数量
                 */
                public int addValidData(List<? super T> targetList, List<? extends T> sourceList, Predicate<T> rule) {
                    // 先判空,避免空指针
                    Objects.requireNonNull(targetList, "目标集合不能为null");
                    Objects.requireNonNull(sourceList, "源数据集合不能为null");
                    
                    int successCount = 0;
                    for (T data : sourceList) {
                        if (rule.test(data)) {
                            targetList.add(data); // 下界通配符,写入安全
                            successCount++;
                        }
                    }
                    return successCount;
                }
            
                /**
                 * 获取校验失败的数据(静态泛型方法:自己定义<T>,避开静态泛型坑)
                 * @param dataList 待校验集合
                 * @param rule 校验规则
                 * @param <T> 数据类型
                 * @return 失败的数据列表
                 */
                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存错误数据(避开泛型异常坑)
                 */
                public static class ValidationException extends RuntimeException {
                    private final Object invalidData; // 存任意类型的错误数据
            
                    public ValidationException(String message, Object invalidData) {
                        super(message);
                        this.invalidData = invalidData;
                    }
            
                    // 泛型方法:安全获取错误数据
                    public <E> E getInvalidData(Class<E> type) {
                        if (type.isInstance(invalidData)) {
                            return type.cast(invalidData);
                        }
                        throw new ClassCastException("错误数据类型不对,想要:" + type.getName());
                    }
                }
            
                /**
                 * 严格校验:失败就抛异常
                 */
                public void strictValidate(List<? extends T> dataList, Predicate<T> rule) {
                    List<T> invalidData = getInvalidData(dataList, rule);
                    if (!invalidData.isEmpty()) {
                        // 抛非泛型异常,带错误数据
                        throw new ValidationException(DEFAULT_ERROR_MSG, invalidData.get(0));
                    }
                }
            
                // 测试方法
                public static void main(String[] args) {
                    // 1. 字符串校验示例(校验非空非空白)
                    GenericDataValidator<String> strValidator = new GenericDataValidator<>(String.class);
                    List<String> strList = List.of("Java", "", "泛型", "   ", "高级特性");
                    // 校验规则:非空且非空白
                    Predicate<String> strRule = s -> s != null && !s.isBlank();
                    
                    // 校验所有数据
                    boolean allValid = strValidator.validateAll(strList, strRule);
                    System.out.println("字符串是否全部有效:" + allValid); // false
                    
                    // 获取无效数据
                    List<String> invalidStr = GenericDataValidator.getInvalidData(strList, strRule);
                    System.out.println("无效字符串:" + invalidStr); // ["", "   "]
                    
                    // 批量写入有效数据到Object集合(下界通配符)
                    List<Object> targetList = new ArrayList<>();
                    int successCount = strValidator.addValidData(targetList, strList, strRule);
                    System.out.println("成功写入有效字符串数量:" + successCount); // 3
                    System.out.println("目标集合内容:" + targetList); // [Java, 泛型, 高级特性]
                    
                    // 严格校验,失败抛异常
                    try {
                        strValidator.strictValidate(strList, strRule);
                    } catch (ValidationException e) {
                        String invalidData = e.getInvalidData(String.class);
                        System.out.println("校验失败,错误数据:" + invalidData); // ""
                    }
            
                    // 2. 整数校验示例(校验大于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("整数是否全部有效:" + intAllValid); // false
                    List<Integer> invalidInt = GenericDataValidator.getInvalidData(intList, intRule);
                    System.out.println("无效整数:" + invalidInt); // [-5]
                }
            }
            

            3. 这个工具类的亮点

            • 通配符用得对:读取数据用? extends T,写入数据用? super T,严格遵守PECS原则,不会有编译报错;
            • 避开了所有类型擦除的坑:用类型令牌判断类型、静态方法自己定义泛型、非泛型异常存错误数据、用List代替数组;
            • 类型安全:所有操作都做了类型校验,不会出现运行时ClassCastException;
            • 实用性强:我在项目里校验表单数据、批量入库数据时,都是直接用这个工具类,改改校验规则就行。

            四、我的泛型避坑清单

            最后给大家整理个避坑清单,都是我踩过的教训,记下来能少走很多弯路:

            坑的场景为啥会坑我咋解决的
            泛型数组创建失败类型擦除后数组分不清泛型类型用List<List>代替泛型数组
            instanceof判断泛型类型运行时没泛型信息传Class类型令牌,遍历判断元素类型
            静态变量用泛型静态变量属于类,没法绑定实例泛型静态方法自己定义泛型参数,静态变量用具体类型
            catch捕获泛型异常运行时泛型信息被擦除,JVM分不清不用泛型异常,用Object存错误数据,手动转型
            泛型方法重载冲突擦除后方法签名一样加Class参数区分,或改方法名
            通配符写入数据报错违反PECS原则读数据用? extends T,写数据用? super T

            五、总结

            泛型这东西,真的不用死记硬背概念,核心就两点:

            • 通配符记住PECS原则——生产者(读数据)用extends,消费者(写数据)用super;
            • 类型擦除记住“编译期管类型,运行时没泛型”,遇到坑就用类型令牌、List代替数组、非泛型异常这些办法补。

            到此这篇关于Java泛型高级玩法之通配符、上下界与类型擦除避坑实战的文章就介绍到这了,更多相关Java泛型内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

            0
            价值2999元 Java视频教程限时免费下载
            专为Java开发者设计,涵盖核心技术、架构设计、性能优化等
            立即下载

            精彩评论

            暂无评论...
            验证码 换一张
            取 消