Java 泛型详解

泛型(Generic)是 Java 5 引入的核心特性,核心目标是将类型参数化,实现「编写一次,适配多种类型」,同时在编译期检查类型安全,避免强制类型转换(减少 ClassCastException)。本文从「基础用法→核心原理→高级特性→常见坑点」全维度拆解,让你彻底掌握泛型。

一、泛型的核心价值

1. 解决的痛点(无泛型的问题)

java

运行

复制代码
// 无泛型的 ArrayList:存储任意类型,取出需强制转换,易出错
List list = new ArrayList();
list.add("Java");
list.add(123);
// 运行时才会报错(ClassCastException)
String s = (String) list.get(1); 

2. 泛型的优势

java

运行

复制代码
// 有泛型:编译期限制类型,无需强制转换,更安全
List<String> list = new ArrayList<>();
list.add("Java");
// 编译期直接报错(类型不匹配),提前规避问题
// list.add(123); 
String s = list.get(0); // 无需强制转换

核心价值总结:

  • 类型安全:编译期检查类型,避免运行时类型转换异常;
  • 代码复用:一套逻辑适配多种类型(如 ArrayList 可存 String/Integer 等);
  • 可读性提升:代码中明确标注类型,语义更清晰。

二、泛型的基础用法

1. 泛型类 / 接口

定义语法

java

运行

复制代码
// 泛型类:<T> 是类型参数(占位符),T 可替换为任意引用类型(String/Integer/自定义类)
public class GenericClass<T> {
    // 泛型成员变量
    private T data;
    
    // 泛型构造方法
    public GenericClass(T data) {
        this.data = data;
    }
    
    // 泛型方法
    public T getData() {
        return data;
    }
    
    public void setData(T data) {
        this.data = data;
    }
}

// 泛型接口
public interface GenericInterface<T> {
    T process(T input);
}
使用示例

java

运行

复制代码
// 实例化时指定具体类型:T = String
GenericClass<String> stringClass = new GenericClass<>("Hello Generic");
System.out.println(stringClass.getData()); // Hello Generic

// T = Integer
GenericClass<Integer> intClass = new GenericClass<>(100);
System.out.println(intClass.getData()); // 100

// 实现泛型接口(指定类型)
class StringProcessor implements GenericInterface<String> {
    @Override
    public String process(String input) {
        return input.toUpperCase();
    }
}
StringProcessor processor = new StringProcessor();
System.out.println(processor.process("java")); // JAVA

2. 泛型方法

定义语法(关键:<T> 放在返回值前)

java

运行

复制代码
public class GenericMethodDemo {
    // 泛型方法:<T> 标识这是泛型方法,T 是类型参数
    public <T> T getFirstElement(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[0];
    }
    
    // 静态泛型方法(同理,<T> 必须加)
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
    }
}
使用示例

java

运行

复制代码
GenericMethodDemo demo = new GenericMethodDemo();
// 自动推导 T = String
String[] strArray = {"a", "b", "c"};
System.out.println(demo.getFirstElement(strArray)); // a

// 自动推导 T = Integer
Integer[] intArray = {1, 2, 3};
demo.printArray(intArray); // 1 2 3

3. 泛型通配符(?)

泛型通配符用于「不确定具体类型」的场景,核心分 3 种:

表格

通配符 含义 示例
? 任意类型(无界通配符) List<?> 可接收任意泛型 List
? extends T 上限通配符:T 或 T 的子类 List<? extends Number> 可接收 List<Integer>/List<Double>
? super T 下限通配符:T 或 T 的父类 List<? super Integer> 可接收 List<Number>/List<Object>
示例:通配符的使用

java

运行

复制代码
// 无界通配符:接收任意类型的 List,但只能读,不能写(除了 null)
public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.print(obj + " ");
    }
}

// 上限通配符:只能读取 Number 及其子类,不能写入(无法确定具体子类)
public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number num : list) {
        total += num.doubleValue();
    }
    return total;
}

// 下限通配符:只能写入 Integer 及其子类,读取只能是 Object
public static void addInteger(List<? super Integer> list) {
    list.add(10);
    list.add(20);
    // 读取时只能转 Object
    Object obj = list.get(0);
}

// 测试
public static void main(String[] args) {
    List<Integer> intList = Arrays.asList(1,2,3);
    List<Double> doubleList = Arrays.asList(1.1,2.2);
    
    printList(intList); // 1 2 3
    System.out.println(sum(doubleList)); // 3.3
    addInteger(new ArrayList<Number>()); // 合法
}

三、泛型的核心原理:类型擦除

1. 什么是类型擦除?

Java 泛型是「编译期特性」,JVM 运行时不存在泛型类型信息,编译器会将泛型代码「擦除」为原生类型(Raw Type),具体规则:

  • 擦除所有类型参数,替换为上限类型(无上限则替换为 Object);
  • 插入必要的强制类型转换;
  • 生成桥接方法(处理泛型继承 / 重写)。

2. 类型擦除示例

编译前代码

java

运行

复制代码
List<String> strList = new ArrayList<>();
strList.add("Java");
String s = strList.get(0);

List<Integer> intList = new ArrayList<>();
intList.add(123);
Integer i = intList.get(0);
编译后(类型擦除)代码

java

运行

复制代码
List strList = new ArrayList();
strList.add("Java");
// 插入强制类型转换
String s = (String) strList.get(0);

List intList = new ArrayList();
intList.add(123);
// 插入强制类型转换
Integer i = (Integer) intList.get(0);

3. 类型擦除的关键结论

  1. 运行时无泛型 :JVM 中 List<String>List<Integer> 是同一个类型(List),可通过反射验证:

    java

    运行

    复制代码
    List<String> strList = new ArrayList<>();
    List<Integer> intList = new ArrayList<>();
    // 输出 true,运行时类型相同
    System.out.println(strList.getClass() == intList.getClass()); 
  2. 泛型不能用于静态变量 :静态变量属于类,类型擦除后无法区分泛型类型:

    java

    运行

    复制代码
    public class Test<T> {
        // 编译报错:静态变量不能引用泛型类型参数
        // private static T data; 
    }
  3. 泛型不能用于数组创建 :类型擦除后数组类型无法确定,编译报错:

    java

    运行

    复制代码
    // 编译报错
    // List<String>[] strArray = new List<String>[10]; 
    // 合法(无泛型数组)
    List[] strArray = new List[10]; 

四、泛型的高级特性

1. 泛型上限(<T extends 类型>)

限制泛型类型必须是指定类型或其子类,支持多上限(类在前,接口在后,用 & 分隔)。

示例:单上限 + 多上限

java

运行

复制代码
// 单上限:T 必须是 Number 或其子类
public class NumberBox<T extends Number> {
    private T value;
    
    public double getDoubleValue() {
        return value.doubleValue();
    }
}

// 多上限:T 必须是 Serializable 且 Comparable 的子类
public class MultiBound<T extends Serializable & Comparable<T>> {
    private T data;
}

// 使用
NumberBox<Integer> intBox = new NumberBox<>();
intBox.setValue(10);
System.out.println(intBox.getDoubleValue()); // 10.0

NumberBox<String> strBox; // 编译报错:String 不是 Number 子类

2. 泛型方法的重载与重写

重载:泛型类型参数不同不算重载(类型擦除后相同)

java

运行

复制代码
public class GenericOverload {
    // 编译报错:类型擦除后都是 Object 参数,方法签名重复
    // public void show(String str) {} 
    // public void show(T t) {} 
}
重写:桥接方法保证重写正确性

java

运行

复制代码
// 父类
public class Parent<T> {
    public T getValue() {
        return null;
    }
}

// 子类
public class Child extends Parent<String> {
    // 重写父类方法(泛型擦除后父类是 Object,子类是 String)
    @Override
    public String getValue() {
        return "Java";
    }
    
    // 编译器自动生成桥接方法(保证重写规则)
    public Object getValue() {
        return getValue(); // 调用 String 版本的 getValue
    }
}

3. 自定义泛型注解(扩展)

java

运行

复制代码
// 定义泛型注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GenericAnnotation<T> {
    Class<T> value();
}

// 使用
public class User {
    @GenericAnnotation(String.class)
    private String name;
    
    @GenericAnnotation(Integer.class)
    private Integer age;
}

五、泛型的常见坑点与避坑指南

1. 坑点 1:泛型类不能实例化类型参数

java

运行

复制代码
public class Test<T> {
    public Test() {
        // 编译报错:类型擦除后 T 是 Object,无法实例化
        // T t = new T(); 
        // 正确方式:通过 Class 对象实例化
    }
    
    // 正确写法
    public T createInstance(Class<T> clazz) throws InstantiationException, IllegalAccessException {
        return clazz.newInstance();
    }
}

2. 坑点 2:泛型异常不能捕获 / 抛出

java

运行

复制代码
// 编译报错:泛型不能用于异常类型
// public class GenericException<T> extends Exception {} 

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

3. 坑点 3:泛型通配符的读写限制

  • ? extends T只读不写(能读 T 类型,无法确定具体子类,写入会报错);
  • ? super T只写不读(能写 T 类型,读取只能是 Object);
  • 记忆口诀:PECS(Producer Extends, Consumer Super)
    • 生产者(提供数据)用 extends(如读取集合元素);
    • 消费者(消费数据)用 super(如向集合写入元素)。

4. 坑点 4:泛型类型不参与重载

如前文所述,类型擦除后泛型参数会被擦除,导致方法签名重复,无法重载。

六、泛型的典型应用场景

1. 集合框架(核心)

Java 集合全部基于泛型实现,如 ArrayList<T>HashMap<K,V>HashSet<T> 等,是泛型最核心的应用。

2. 自定义工具类

java

运行

复制代码
// 泛型工具类:通用转换器
public class ConverterUtil {
    // 将 List<S> 转换为 List<T>
    public static <S, T> List<T> convert(List<S> source, Function<S, T> converter) {
        List<T> target = new ArrayList<>();
        for (S s : source) {
            target.add(converter.apply(s));
        }
        return target;
    }
}

// 使用
List<String> strList = Arrays.asList("1", "2", "3");
// 转换为 Integer 列表
List<Integer> intList = ConverterUtil.convert(strList, Integer::valueOf);

3. 通用返回结果(接口开发)

java

运行

复制代码
// 泛型返回结果:适配所有接口返回类型
public class Result<T> {
    private int code;
    private String msg;
    private T data;
    
    // 静态工厂方法
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("success");
        result.setData(data);
        return result;
    }
    
    public static <T> Result<T> fail(String msg) {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }
    
    // getter/setter 省略
}

// 使用
Result<User> userResult = Result.success(new User("张三", 20));
Result<List<User>> listResult = Result.success(Arrays.asList(userResult.getData()));

七、常见面试题

  1. **泛型的作用?**编译期类型安全、避免强制类型转换、代码复用。
  2. **什么是类型擦除?**Java 泛型仅存在编译期,运行时泛型类型被擦除为上限类型(Object),JVM 无泛型信息。
  3. 泛型通配符?extends T 和?super T 的区别? extends 是上限,只读不写;super 是下限,只写不读(PECS 原则)。
  4. **为什么不能创建泛型数组?**类型擦除后数组类型无法确定,运行时会破坏类型安全。
  5. **泛型方法和泛型类的区别?**泛型类的类型参数作用于整个类,泛型方法的类型参数仅作用于方法(可在非泛型类中定义)。

总结

  1. 核心本质:泛型是 Java 编译期特性,通过「类型参数化」实现类型安全和代码复用,运行时通过类型擦除变为原生类型;
  2. 核心用法:泛型类 / 接口、泛型方法、通配符(?/extends/super),遵循 PECS 原则;
  3. 核心避坑:类型擦除导致的运行时无泛型、不能实例化泛型参数、不能创建泛型数组。
相关推荐
阿钱真强道1 小时前
31 Python 聚类:层次聚类怎么理解?AGNES 和 DIANA 有什么区别?
python·聚类·层次聚类·diana·agnes
桃气媛媛1 小时前
python流程控制-匹配语句match
开发语言·python
羊小猪~~1 小时前
算法/力扣--数组典型题目
c语言·c++·python·算法·leetcode·职场和发展·求职招聘
龙文浩_2 小时前
【无标题】AI深层神经网络(多层全连接)+ ReLU 激活 的完整处理流程
人工智能·python·深度学习·神经网络·机器学习
ulias2122 小时前
C++ 异常处理机制
java·开发语言·c++
小手智联老徐2 小时前
Windows 下 ADB 无线调试与系统级操作指南
android·windows·adb
大阿明2 小时前
Spring BOOT 启动参数
java·spring boot·后端
add45a2 小时前
Python类型提示(Type Hints)详解
jvm·数据库·python
程序员小郭832 小时前
Spring Ai 05 ChatClient Advisor 实战(日志、提示词增强、内容安全)
java·开发语言·前端