Java 泛型(Generic)& 通配符(Wildcard)详解

Java 泛型是 Java 5 引入的核心特性,本质是 “参数化类型”—— 允许在定义类、接口、方法时,将类型作为 “参数” 传入,使用时再指定具体类型。核心价值是 在编译时强制类型安全,避免类型转换错误,同时实现代码复用。

通配符(?)是泛型的补充,用于 灵活匹配未知类型,解决泛型类型之间的兼容性问题(如 List<String>List<Object> 不兼容),常见于集合、框架工具类的参数声明。

一、泛型概述:为什么需要泛型?

在没有泛型的 Java 1.4 及之前,只能通过 Object 类型实现 “通用” 逻辑,但存在两大问题:

  1. 类型不安全:可向集合中添加任意类型对象,运行时才会抛出类型转换异常;

  2. 冗余的强制转换:取出数据时必须手动强制转换,代码繁琐且易出错。

示例:无泛型 vs 有泛型

// 1. 无泛型(Java 1.4 及之前)
List list = new ArrayList();
list.add("Java");       // 存入 String
list.add(123);          // 存入 Integer(编译不报错,运行时可能出问题)
String str = (String) list.get(1); // 运行时抛出 ClassCastException
​
// 2. 有泛型(Java 5+)
List<String> strList = new ArrayList<>();
strList.add("Java");    // 仅允许存入 String
// strList.add(123);    // 编译直接报错(类型安全)
String str2 = strList.get(0); // 无需强制转换(编译时确定类型)

泛型的核心优势

  1. 类型安全:编译时检查类型,禁止存入不匹配的类型;

  2. 消除强制转换:编译器自动推断类型,取出数据无需手动转换;

  3. 代码复用:同一套逻辑可适配多种类型(如 ArrayList<String>ArrayList<Integer> 共用 ArrayList 类);

  4. 可读性强:代码中明确指定类型,语义清晰。

二、泛型的核心用法

泛型的使用场景主要分为三类:泛型类、泛型接口、泛型方法,核心语法是用 <类型参数> 声明(常用 TEKV 等作为类型参数名,仅为约定,可自定义)。

1. 泛型类:类定义时声明泛型参数

泛型类是指在类名后声明泛型参数,实例化时指定具体类型。

语法结构
// 声明泛型类:<T> 是类型参数(T = Type,可替换为 E/K/V 等)
public class 类名<T> {
    // 泛型字段(不能是 static,因为泛型类型实例化时确定)
    private T data;
​
    // 泛型构造器
    public 类名(T data) {
        this.data = data;
    }
​
    // 泛型方法(返回值为泛型类型)
    public T getData() {
        return data;
    }
}
实战案例:自定义泛型容器
// 泛型类:通用数据容器
public class GenericBox<T> {
    private T content;
​
    // 泛型构造器
    public GenericBox(T content) {
        this.content = content;
    }
​
    // 泛型方法:设置数据
    public void setContent(T content) {
        this.content = content;
    }
​
    // 泛型方法:获取数据
    public T getContent() {
        return content;
    }
​
    public static void main(String[] args) {
        // 实例化时指定具体类型:String 类型
        GenericBox<String> stringBox = new GenericBox<>("Hello Generic");
        String str = stringBox.getContent(); // 无需强制转换
        System.out.println("String 容器:" + str);
​
        // 实例化时指定具体类型:Integer 类型
        GenericBox<Integer> intBox = new GenericBox<>(100);
        int num = intBox.getContent();
        System.out.println("Integer 容器:" + num);
​
        // 错误:不能存入不匹配的类型(编译报错)
        // stringBox.setContent(123);
    }
}

2. 泛型接口:接口定义时声明泛型参数

泛型接口与泛型类语法类似,实现接口时需指定具体类型,或继续保留泛型参数。

语法结构
public interface 接口名<T> {
    T process(T data); // 泛型方法
}
实战案例:泛型生成器接口
// 泛型接口:生成器(生成指定类型的对象)
public interface Generator<T> {
    T generate(); // 生成 T 类型对象
}
​
// 实现接口:指定具体类型(String 生成器)
class StringGenerator implements Generator<String> {
    @Override
    public String generate() {
        return "随机字符串:" + Math.random();
    }
}
​
// 实现接口:保留泛型参数(通用数字生成器)
class NumberGenerator<T extends Number> implements Generator<T> {
    private final Class<T> clazz;
​
    public NumberGenerator(Class<T> clazz) {
        this.clazz = clazz;
    }
​
    @Override
    public T generate() throws IllegalAccessException, InstantiationException {
        // 简单示例:通过反射生成实例(实际需根据类型处理)
        return clazz.newInstance();
    }
}
​
// 测试
public class GenericInterfaceDemo {
    public static void main(String[] args) throws Exception {
        Generator<String> stringGen = new StringGenerator();
        System.out.println(stringGen.generate());
​
        Generator<Integer> intGen = new NumberGenerator<>(Integer.class);
        System.out.println(intGen.generate()); // 输出 Integer 默认值 0
    }
}

3. 泛型方法:方法单独声明泛型参数

泛型方法是指 方法本身声明泛型参数(与类 / 接口的泛型无关),即使所在类不是泛型类,也可定义泛型方法。

核心语法
// 泛型方法:<T> 必须放在返回值前,声明方法的泛型参数
public <T> T 方法名(T 参数) {
    // 逻辑
}
关键特点
  • 泛型方法的 <T> 是独立的,与类的泛型参数(若有)无关;

  • 静态方法不能使用类的泛型参数,需定义为泛型方法;

  • 调用时可省略泛型声明(Java 7+ 支持类型推断)。

实战案例:泛型工具方法
public class GenericMethodDemo {
    // 泛型方法:交换数组中两个位置的元素(适配任意类型数组)
    public static <T> void swap(T[] array, int i, int j) {
        if (i < 0 || i >= array.length || j < 0 || j >= array.length) {
            return;
        }
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
​
    // 泛型方法:获取数组中第一个元素
    public static <T> T getFirstElement(T[] array) {
        return array.length > 0 ? array[0] : null;
    }
​
    public static void main(String[] args) {
        // 测试 String 数组
        String[] strArray = {"A", "B", "C"};
        swap(strArray, 0, 2); // 类型推断:T=String,无需显式声明
        System.out.println("交换后:" + Arrays.toString(strArray)); // [C, B, A]
        System.out.println("第一个元素:" + getFirstElement(strArray)); // C
​
        // 测试 Integer 数组
        Integer[] intArray = {1, 2, 3};
        swap(intArray, 1, 2);
        System.out.println("交换后:" + Arrays.toString(intArray)); // [1, 3, 2]
    }
}

三、泛型边界限定:<T extends ...>

默认情况下,泛型参数 T 可以是任意引用类型(如 StringInteger、自定义类)。但有时需要限制 T 的范围(如仅允许数字类型),此时需使用 边界限定

核心语法

// 上界限定:T 必须是「类A」或其子类(类A可以是类、接口)
<T extends 类A>
​
// 多边界限定:T 必须实现多个接口(类在前,接口在后,用 & 连接)
<T extends 类A & 接口B & 接口C>

关键说明

  • 上界限定用 extends(无论类还是接口,统一用 extends,不用 implements);

  • 限定后,可在泛型中调用上界类型的方法(类型安全);

  • 最多只能有一个类作为上界(若有类,必须放在第一个),接口可多个。

实战案例:泛型边界限定

// 泛型类:仅支持 Number 及其子类(Integer、Double 等)
public class NumberProcessor<T extends Number> {
    // 计算数组元素的总和(可调用 Number 的 doubleValue() 方法)
    public double sum(T[] array) {
        double total = 0.0;
        for (T num : array) {
            total += num.doubleValue(); // 调用上界 Number 的方法
        }
        return total;
    }

    public static void main(String[] args) {
        // 正确:Integer 是 Number 子类
        NumberProcessor<Integer> intProcessor = new NumberProcessor<>();
        Integer[] intArray = {1, 2, 3};
        System.out.println("整数总和:" + intProcessor.sum(intArray)); // 6.0

        // 正确:Double 是 Number 子类
        NumberProcessor<Double> doubleProcessor = new NumberProcessor<>();
        Double[] doubleArray = {1.5, 2.5, 3.5};
        System.out.println("小数总和:" + doubleProcessor.sum(doubleArray)); // 7.5

        // 错误:String 不是 Number 子类(编译报错)
        // NumberProcessor<String> stringProcessor = new NumberProcessor<>();
    }
}

// 多边界限定:T 必须是 CharSequence 且实现 Comparable
class MultiBoundDemo<T extends CharSequence & Comparable<T>> {
    public int compare(T a, T b) {
        return a.compareTo(b); // 调用 Comparable 的方法
    }

    public static void main(String[] args) {
        MultiBoundDemo<String> demo = new MultiBoundDemo<>();
        System.out.println(demo.compare("A", "B")); // -1(String 满足双边界)
    }
}

四、通配符详解:?? extends T? super T

通配符(?)是泛型的 “占位符”,用于 匹配未知的泛型类型,解决泛型类型的兼容性问题(如 List<String> 不能赋值给 List<Object>)。

核心问题:泛型的 “不可变性”

Java 泛型是 不可变的(Invariant),即 List<String> 不是 List<Object> 的子类,即使 StringObject 的子类。例如:

List<String> strList = new ArrayList<>();
// 错误:编译报错(泛型不可变)
// List<Object> objList = strList;

原因:若允许赋值,objList 可添加 IntegerObject 子类),导致 strList 中混入非 String 类型,破坏类型安全。

通配符的核心作用:在不破坏类型安全的前提下,实现泛型类型的灵活匹配。

1. 无界通配符:?(匹配任意类型)

无界通配符 ? 表示 “任意引用类型”,适用于 仅读取数据,不修改数据 的场景(如打印集合元素)。

语法与场景
// 无界通配符:List<?> 可接收 List<String>、List<Integer> 等任意泛型 List
public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}
关键限制
  • 可读取数据(返回 Object 类型,需按需转换);

  • 不可添加数据(除 null 外),因为无法确定集合的具体类型,添加任何类型都可能破坏类型安全。

实战案例
public class UnboundedWildcardDemo {
    // 无界通配符:打印任意类型的集合
    public static void printCollection(Collection<? extends Object> list) {
    public static void printCollection(Collection<?> collection) {
        for (Object obj : collection) {
            System.out.println(obj);
        }
    }

    public static void main(String[] args) {
        List<String> strList = Arrays.asList("A", "B", "C");
        List<Integer> intList = Arrays.asList(1, 2, 3);

        // 无界通配符适配任意泛型集合
        printCollection(strList); // 打印 A、B、C
        printCollection(intList); // 打印 1、2、3

        List<?> wildcardList = strList;
        // 错误:不可添加非 null 数据(编译报错)
        // wildcardList.add("D");
        wildcardList.add(null); // 仅允许添加 null
    }
}

2. 上界通配符:? extends T(匹配 T 及其子类)

上界通配符 ? extends T 表示 “T 或 T 的子类”,适用于 仅读取数据(生产者) 的场景(如获取集合中的元素,计算最大值)。

语法与场景
// 上界通配符:List<? extends Number> 可接收 List<Integer>、List<Double> 等
public static double sumList(List<? extends Number> list) {
    double total = 0.0;
    for (Number num : list) {
        total += num.doubleValue(); // 可调用 T(Number)的方法
    }
    return total;
}
关键限制
  • 可读取数据(返回 T 类型,类型安全);

  • 不可添加数据(除 null 外),因为无法确定集合的具体子类型(如 List<? extends Number> 可能是 List<Integer>List<Double>,添加 Integer 可能破坏 Double 集合)。

实战案例
public class BoundedUpperWildcardDemo {
    // 上界通配符:计算 Number 子类集合的总和
    public static double calculateSum(List<? extends Number> list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<Double> doubleList = Arrays.asList(1.5, 2.5);

        // 适配 Number 子类集合
        System.out.println("整数总和:" + calculateSum(intList)); // 6.0
        System.out.println("小数总和:" + calculateSum(doubleList)); // 4.0

        List<? extends Number> numList = intList;
        // 错误:不可添加数据(编译报错)
        // numList.add(4);
    }
}

3. 下界通配符:? super T(匹配 T 及其父类)

下界通配符 ? super T 表示 “T 或 T 的父类”,适用于 仅修改数据(消费者) 的场景(如向集合中添加元素)。

语法与场景
// 下界通配符:List<? super Integer> 可接收 List<Integer>、List<Number>、List<Object>
public static void addIntegers(List<? super Integer> list) {
    list.add(1); // 可添加 T(Integer)及其子类(如 int 自动装箱)
    list.add(2);
}
关键限制
  • 可添加数据(仅允许添加 T 及其子类,类型安全);

  • 读取数据时返回 Object 类型(需强制转换,不推荐读取)。

实战案例
public class BoundedLowerWildcardDemo {
    // 下界通配符:向集合中添加 Integer 元素
    public static void fillWithIntegers(List<? super Integer> list) {
        list.add(10);
        list.add(20);
        list.add(30);
    }

    public static void main(String[] args) {
        // 场景 1:List<Integer>(T 本身)
        List<Integer> intList = new ArrayList<>();
        fillWithIntegers(intList);
        System.out.println("Integer 集合:" + intList); // [10, 20, 30]

        // 场景 2:List<Number>(T 的父类)
        List<Number> numList = new ArrayList<>();
        fillWithIntegers(numList);
        numList.add(40.5); // 可添加其他 Number 子类
        System.out.println("Number 集合:" + numList); // [10, 20, 30, 40.5]

        // 场景 3:List<Object>(T 的祖父类)
        List<Object> objList = new ArrayList<>();
        fillWithIntegers(objList);
        objList.add("Hello"); // 可添加其他 Object 子类
        System.out.println("Object 集合:" + objList); // [10, 20, 30, Hello]

        List<? super Integer> superList = objList;
        // 正确:可添加 Integer 及其子类
        superList.add(40);
        // 错误:不可添加父类类型(如 Number)
        // superList.add(50.5);

        // 读取数据:返回 Object,需强制转换
        Object obj = superList.get(0);
        Integer num = (Integer) obj;
    }
}

4. 通配符核心原则:PECS

PECS 是 Producer Extends, Consumer Super 的缩写,是选择通配符的黄金法则:

  • Producer(生产者)

    :仅提供数据(读取),用 ? extends T (上界通配符);

    • 例:List<? extends Number> 是 Number 的 “生产者”,只能读取 Number 类型数据;

  • Consumer(消费者)

    :仅接收数据(写入),用 ? super T (下界通配符);

    • 例:List<? super Integer> 是 Integer 的 “消费者”,只能写入 Integer 类型数据;

  • 若既读又写:不使用通配符,直接指定具体泛型类型(如 List<Integer>)。

PECS 原则示例
// 生产者:读取数据(extends)
public static <T> T getMax(List<? extends T> list, Comparator<T> comparator) {
    if (list.isEmpty()) return null;
    T max = list.get(0);
    for (T t : list) {
        if (comparator.compare(t, max) > 0) {
            max = t;
        }
    }
    return max;
}

// 消费者:写入数据(super)
public static <T> void addAll(List<? super T> dest, List<? extends T> src) {
    for (T t : src) { // src 是生产者(extends)
        dest.add(t);   // dest 是消费者(super)
    }
}

五、泛型的常见误区与限制

1. 类型擦除(Type Erasure)

Java 泛型是 编译时特性,编译后泛型信息会被 “擦除”:

  • 无边界泛型(T)擦除为 Object

  • 有上界泛型(T extends Number)擦除为上界类型(Number)。

类型擦除的影响
public class TypeErasureDemo {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();

        // 编译后泛型信息被擦除,两者类型相同(都是 List)
        System.out.println(strList.getClass() == intList.getClass()); // true

        // 错误:不能用 instanceof 判断泛型类型(编译报错)
        // if (strList instanceof List<String>) {}
    }
}

2. 不能创建泛型数组

泛型数组会破坏类型安全(数组是协变的,而泛型是不可变的),编译直接报错:

// 错误:不能创建泛型数组(编译报错)
// List<String>[] strListArray = new List<String>[10];

// 正确:用无界通配符数组(但需谨慎使用)
List<?>[] wildcardArray = new List<?>[10];
wildcardArray[0] = new ArrayList<String>();

3. 静态成员不能依赖泛型参数

泛型参数是实例级别的(每个实例的泛型类型可能不同),静态成员属于类级别,无法依赖实例的泛型参数:

public class GenericStaticDemo<T> {
    // 错误:静态字段不能使用泛型参数(编译报错)
    // private static T staticData;

    // 正确:静态泛型方法(自己声明泛型参数)
    public static <E> E staticGenericMethod(E data) {
        return data;
    }
}

4. 不能捕获泛型异常

泛型类型不能作为异常类型在 catch 中使用:

// 错误:不能捕获泛型异常(编译报错)
// try {
// } catch (T e) {
// }

六、泛型在实际开发中的应用

1. 集合框架(最常用)

Java 集合框架(ArrayListHashMapHashSet 等)全部基于泛型实现,例如:

// HashMap<K, V>:键值对泛型
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("张三", 90);
scoreMap.put("李四", 85);
Integer score = scoreMap.get("张三"); // 无需强制转换

2. 自定义工具类

泛型可实现通用工具类,适配多种类型,例如:

// 泛型工具类:数组工具
public class ArrayUtils {
    // 泛型方法:数组转集合
    public static <T> List<T> toList(T[] array) {
        return Arrays.asList(array);
    }

    // 泛型方法:集合转数组
    public static <T> T[] toArray(List<T> list, T[] array) {
        return list.toArray(array);
    }
}

3. 框架开发(如 Spring、MyBatis)

框架大量使用泛型简化 API,例如:

  • Spring 的 JdbcTemplatequeryForObject(String sql, Class<T> requiredType)

  • MyBatis 的 Mapper 接口:public interface UserMapper { User selectById(Integer id); }(泛型适配返回值类型)。

总结

泛型与通配符的核心是 “类型安全 + 代码复用”

  1. 泛型

    :通过参数化类型,编译时强制类型安全,消除强制转换;

    • 核心用法:泛型类、泛型接口、泛型方法;

    • 边界限定:<T extends ...> 限制泛型类型范围。

  2. 通配符

    :解决泛型不可变性问题,灵活匹配未知类型;

    • 无界 ?:适配任意类型,仅读不写;

    • 上界 ? extends T:生产者,仅读不写;

    • 下界 ? super T:消费者,仅写不读;

    • 黄金法则:PECS(Producer Extends, Consumer Super)。

  3. 常见限制:类型擦除、不能创建泛型数组、静态成员不依赖泛型参数。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐