Java中的泛型

Java泛型是JDK 5引入的核心特性,看似简单实则暗藏玄机。本文将从类型擦除机制、通配符应用、PECS原则及实战避坑四个维度进行深度解析。

为什么需要泛型

泛型引入前的问题

java 复制代码
// Java 5之前的代码
List list = new ArrayList();
list.add("Hello");
list.add(123);  // 编译通过,但运行时可能出错

String str = (String) list.get(0);  // 需要强制转换
Integer num = (Integer) list.get(1);  // 可能抛出ClassCastException

痛点

类型不安全,运行时才暴露问题

需要大量强制类型转换

代码可读性差

泛型引入后的改进

java 复制代码
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123);  // 编译错误!

String str = list.get(0);  // 无需强制转换

优势

类型安全检查前置到编译期

消除强制类型转换

代码更清晰、更安全

泛型的底层机制

类型擦除的本质

类型擦除是Java泛型实现的核心机制,在编译期将泛型类型参数替换为边界类型,字节码中不保留泛型信息。

java 复制代码
// 源代码
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0);

// 编译后(字节码层面)
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0);  // 自动插入强制转换

类型擦除的规则

泛型声明 擦除后类型
List<T> List (T无边界→Object)
List<T extends Number> List (T有边界→Number)
List<T extends Comparable<T>> List (T有边界→Comparable)

类型擦除带来的限制

java 复制代码
// ❌ 坑1:无法创建泛型数组
List<String>[] array = new List<String>[10];  // 编译错误

// ✅ 解决方案:使用ArrayList或通配符
List<List<String>> list = new ArrayList<>();

// ❌ 坑2:无法instanceof泛型类型
if (obj instanceof List<String>) {}  // 编译错误

// ✅ 解决方案:检查原始类型
if (obj instanceof List) {}

// ❌ 坑3:泛型类型不能用于静态成员
class Generic<T> {
    static T value;  // 编译错误
    static List<T> list;  // 编译错误
}

// ❌ 坑4:不能new泛型类型实例
class Box<T> {
    T create() {
        return new T();  // 编译错误
    }
}

// ✅ 解决方案:传入Class对象
T create(Class<T> clazz) {
    return clazz.newInstance();
}

// ❌ 坑5:List<String>不是List<Object>的子类
List<String> strings = new ArrayList<>();
List<Object> objects = strings;  // 编译错误!

通配符解决类型擦除灵活性

三种通配符对比

通配符类型 语法 读取 写入 使用场景
无界通配符 <?> Object 不能写(null除外) 只读操作
上界通配符 <? extends T> T或子类 不能写(null除外) 生产者(读)
下界通配符 <? super T> Object T或子类 消费者(写)

无界通配符<?>

java 复制代码
// 场景:通用集合打印工具
public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}

// 可以使用
printList(new ArrayList<String>());
printList(new ArrayList<Integer>());

// 不能添加元素(null除外)
list.add("hello");  // 编译错误
list.add(null);  // 可以

上界通配符 <? extends T>

java 复制代码
// 场景:数值集合求和工具
public static double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
    return total;
}

// 可以使用
sum(new ArrayList<Integer>());
sum(new ArrayList<Double>());
sum(new ArrayList<Number>());

// ❌ 不能添加元素(null除外)
numbers.add(1);  // 编译错误

为什么不能写? 因为编译器不知道具体是哪个子类:

java 复制代码
List<? extends Number> list = new ArrayList<Integer>();
list.add(new Double(1.0));  // 如果允许,会破坏类型安全!

下界通配符 <? super T>

java 复制代码
// 场景:数据批量插入工具
public static void addNumbers(List<? super Integer> list) {
    for (int i = 0; i < 10; i++) {
        list.add(i);  // 可以添加Integer及其子类
    }
}

// 可以使用
addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Number>());
addNumbers(new ArrayList<Object>());

// ⚠️ 读取只能得到Object
Object obj = list.get(0);
Integer num = (Integer) list.get(0);  // 需要强制转换

PECS原则:通配符使用黄金法则

PECS = Producer Extends, Consumer Super

角色 通配符 记忆方法
生产者(Producer) <? extends T> 从集合读取数据,用extends
消费者(Consumer) <? super T> 向集合写入数据,用super

实战示例:Collections.copy

java 复制代码
// JDK源码中的经典应用
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    // dest是消费者(写入)→ ? super T
    // src是生产者(读取)→ ? extends T
}

// 使用示例
List<Integer> src = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>();
Collections.copy(dest, src);  // 正确

完整实战:通用数据处理工具类

java 复制代码
public class GenericUtils {
    
    // 生产者:只读取
    public static <T> T getFirst(List<? extends T> list) {
        return list.isEmpty() ? null : list.get(0);
    }
    
    // 消费者:只写入
    public static <T> void addAll(List<? super T> dest, List<? extends T> src) {
        for (T item : src) {
            dest.add(item);
        }
    }
    
    // 既读又写:不使用通配符
    public static <T> void swap(List<T> list, int i, int j) {
        T temp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, temp);
    }
}

泛型类、接口与方法

泛型类

java 复制代码
public class Box<T> {
    private T value;
    
    public void set(T value) {
        this.value = value;
    }
    
    public T get() {
        return value;
    }
}

// 使用
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get();

泛型接口

java 复制代码
public interface Comparable<T> {
    int compareTo(T other);
}

public class Person implements Comparable<Person> {
    private String name;
    
    @Override
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }
}

泛型方法

java 复制代码
// 泛型方法:类型参数在返回值前声明
public static <T> T getMiddle(T[] array) {
    return array[array.length / 2];
}

// 调用时类型可推断
String[] strings = {"a", "b", "c"};
String middle = getMiddle(strings);

高级技巧与最佳实践

泛型方法 vs 泛型类

java 复制代码
// 优先使用泛型方法(更灵活)
public static <T> List<T> asList(T... items) {
    return Arrays.asList(items);
}

// 而不是让调用者指定类类型参数

类型安全的异构容器

java 复制代码
// 使用Class作为类型令牌
public class Container {
    private Map<Class<?>, Object> data = new HashMap<>();
    
    public <T> void put(Class<T> type, T value) {
        data.put(type, value);
    }
    
    public <T> T get(Class<T> type) {
        return type.cast(data.get(type));
    }
}

// 使用
Container container = new Container();
container.put(String.class, "Hello");
container.put(Integer.class, 123);

String str = container.get(String.class);
Integer num = container.get(Integer.class);

避免的常见误区

误区 正确做法
List<Object> 可以接收任何List 使用 List<?>List<? extends T>
泛型信息运行时可用 使用TypeToken或Class令牌
可以创建泛型数组 使用 List<List<T>> 替代
<? extends T> 可以写入 只能读取(null除外)

总结

类型擦除:编译期替换为边界类型,运行时泛型信息不可见

通配符:<?> <? extends T> <? super T>

PECS原则:生产者extends,消费者super

限制:不能new泛型、不能泛型数组、不能instanceof泛型

最佳实践:优先泛型方法、使用Class令牌、避免裸类型

相关推荐
计算机学姐1 小时前
基于SpringBoot的服装购物商城销售系统【协同过滤推荐算法+数据可视化统计】
java·vue.js·spring boot·mysql·信息可视化·mybatis·推荐算法
0 0 01 小时前
CCF-CSP 33-2 相似度计算(jaccard)【C++】考点:STL容器(set/map)
开发语言·c++·算法
Mr YiRan2 小时前
C++高级之SLT中的容器学习与函数谓词
开发语言·c++·学习
弹简特2 小时前
【JavaEE10-后端部分】SpringMVC05-综合案例1-从加法计算器看前后端交互:接口文档与HTTP通信详解
java·spring boot·spring·http
wjs20242 小时前
Bootstrap 下拉菜单:功能、应用与优化
开发语言
tod1132 小时前
C++ 核心知识点全解析(七)
开发语言·c++·面试经验
njsgcs2 小时前
py不等于python
开发语言·python
沐知全栈开发2 小时前
Vue3 事件处理
开发语言
予枫的编程笔记2 小时前
【Kafka进阶篇】Kafka延迟请求处理核心:时间轮算法拆解,比DelayQueue高效10倍
java·kafka·高并发·时间轮算法·delayqueue·延迟任务·timingwheel