本文是笔者系统学习Java泛型的完整笔记,涵盖了从基础语法到底层原理的全部知识点。无论你是初次接触泛型的新手,还是想查漏补缺的老手,相信都能从中受益。

一、背景:为什么需要泛型?

在Java 1.5(2004年)引入泛型之前,我们是这样写代码的:

// 没有泛型的时代
public class Box {
    private Object content;
    
    public void setContent(Object content) {
        this.content = content;
    }
    
    public Object getContent() {
        return content;
    }
}

// 使用时的问题
Box box = new Box();
box.setContent("hello");
// 取出时必须强转,且容易出错
String str = (String) box.getContent(); 
// 如果误存入其他类型,编译期无法发现
box.setContent(123);
String wrong = (String) box.getContent(); // 运行时ClassCastException!

泛型的核心价值

痛点泛型解决方案
类型不安全编译时类型检查,提前发现错误
强制转换繁琐消除强制转换,代码更简洁
代码复用性差类型参数化,一套代码服务多种类型

泛型的本质是将"类型"作为参数,让代码在编译时就确定类型,既保证了类型安全,又实现了代码复用。


二、泛型类与泛型接口

2.1 泛型类的定义与使用

// 定义泛型类,T是类型参数(Type Parameter)
public class Box<T> {
    private T content;
    
    public void setContent(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
}

// 使用泛型类
Box<String> stringBox = new Box<>();  // 钻石操作符<>,类型推断
stringBox.setContent("Hello泛型");
String str = stringBox.getContent();  // 无需强转!

// 尝试放入错误类型,编译报错
// stringBox.setContent(123); // ❌ 编译错误

2.2 泛型接口

// 泛型接口
public interface Generator<T> {
    T generate();
}

// 实现方式1:实现类保留泛型
public class GenericGenerator<T> implements Generator<T> {
    private T data;
    public GenericGenerator(T data) { this.data = data; }
    @Override
    public T generate() { return data; }
}

// 实现方式2:实现类指定具体类型
public class StringGenerator implements Generator<String> {
    @Override
    public String generate() { return "Hello"; }
}

2.3 继承中的泛型规则

子类继承泛型父类时,父类的泛型参数必须"落地":

// 父类
class Generic<T> {
    private T data;
    public T getData() { return data; }
}

// 方式1:子类也保留泛型,类型一致
class ChildGeneric1<T> extends Generic<T> { }

// 方式2:子类明确具体类型
class ChildGeneric2 extends Generic<String> { }

// ❌ 错误写法:子类不泛型,父类类型也不明确
// class ChildGeneric3 extends Generic { } // 编译报错


三、泛型方法

3.1 什么是泛型方法?

泛型方法是在方法声明时定义了自己的类型参数的方法,与类是否泛型无关。

public class Utility {
    // 泛型方法:<T>声明类型参数,位于返回值之前
    public static <T> T getMiddle(T... args) {
        return args[args.length / 2];
    }
    
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
    }
}

// 使用
String middle = Utility.getMiddle("A", "B", "C");  // T推断为String
Utility.printArray(new Integer[]{1, 2, 3});        // T推断为Integer

3.2 为什么必须写 <T>

关键问题:方法的类型参数必须在使用前声明

// ❌ 编译报错:找不到符号 T
public static T getMiddle(T... args) { return args[0]; }

// ✅ 正确:<T>声明了类型参数
public static <T> T getMiddle(T... args) { return args[0]; }

类比:就像使用变量前必须先声明一样,使用类型参数前也必须先声明。

3.3 泛型方法 vs 使用Object

对比项泛型方法使用Object
类型安全✅ 编译时检查❌ 运行时才暴露
消除强转❌ 需要手动强转
类型一致性✅ 保证❌ 无法保证
// 泛型方法:类型安全,无需强转
public static <T> T getFirst(List<T> list) { return list.get(0); }

// 使用Object:需要强转,可能出错
public static Object getFirst(List<?> list) { return list.get(0); }
// 调用:String s = (String) getFirst(list); // 强转,有风险

3.4 泛型类中的静态方法

重要规则:静态方法不能使用类的泛型参数。

public class Box<T> {
    private T content;
    
    // ✅ 非静态方法可以使用 T
    public T getContent() { return content; }
    
    // ❌ 编译报错:静态方法不能使用类的泛型参数
    // public static void staticMethod(T t) { }
    
    // ✅ 静态方法可以有自己的类型参数
    public static <U> void staticMethod(U u) {
        System.out.println(u);
    }
}

原因:类的泛型参数在创建对象时确定,而静态方法在类加载时就存在,生命周期不匹配。


四、类型通配符(Wildcard)

4.1 为什么需要通配符?

泛型是不变(invariant)的:Box<Apple> 和 Box<Fruit> 没有继承关系,即使 Apple extends Fruit

Box<Fruit> fruitBox = new Box<>();
Box<Apple> appleBox = new Box<>();

// ❌ 编译报错:类型不匹配
// fruitBox = appleBox;

// 想让一个方法处理所有水果箱子,就需要通配符
public static void printBox(Box<Fruit> box) { } // 只能接收 Box<Fruit>
public static void printBox(Box<?> box) { }      // 可以接收任何 Box

4.2 无界通配符 ?java

public static void printBox(Box<?> box) {
    System.out.println(box.getContent());
    // box.setContent("hello"); // ❌ 不能写入具体类型(除了null)
}

// 可以传入任何类型的Box
printBox(new Box<String>());
printBox(new Box<Integer>());

4.3 上界通配符 ? extends T

// Box里装的是Fruit或其子类
public static void printFruit(Box<? extends Fruit> box) {
    Fruit f = box.getContent(); // ✅ 读取:一定是Fruit
    // box.setContent(new Apple()); // ❌ 不能写入
}

规则? extends T → 只能读,不能写(除null)。

4.4 下界通配符 ? super Tjava

// Box里装的是Apple或其父类
public static void addApple(Box<? super Apple> box) {
    box.setContent(new Apple()); // ✅ 可以写入Apple
    // Apple a = box.getContent(); // ❌ 读取只能是Object
}

规则? super T → 只能写,读出来是Object

4.5 PECS原则(终极口诀)

Producer Extends, Consumer Super

场景推荐写法原因
生产者:从容器中读取数据? extends T保证读取到的一定是T或其子类
消费者:向容器中写入数据? super T保证能写入T及其子类
既读又写不要用通配符,用 <T>需要保持类型一致性

4.6 T vs ? 的区别

对比T(类型参数)?(通配符)
本质声明一个类型变量使用一个未知类型
能否多次使用✅ 可以❌ 每次独立
作用范围整个类或方法仅当前声明位置
能否写入✅ 可以受限(取决于上下界)
// T:多次使用,保持类型一致
public static <T> void swap(Box<T> box1, Box<T> box2) {
    T temp = box1.getContent();
    box1.setContent(box2.getContent());
    box2.setContent(temp);
}

// ?:每次独立,不能多次引用
public static void process(Box<?> box1, Box<?> box2) {
    // ? temp = box1.getContent(); // ❌ 编译错误
}


五、类型擦除(Type Erasure)

5.1 什么是类型擦除?

核心定义:Java泛型是编译时的概念,在运行时,泛型类型信息会被编译器擦除,替换为原始类型。

// 编译时
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();

// 运行时(字节码)
List stringList = new ArrayList();
List integerList = new ArrayList();
// 两者在运行时完全相同!

5.2 为什么设计成擦除式?

为了向后兼容:Java 1.5才引入泛型,为了能让旧代码无缝迁移,必须让新代码在运行时和旧代码保持一致。

5.3 擦除的具体规则

擦除规则示例
无界类型 → ObjectT → Object
有界类型 → 第一个边界T extends Number → Number
多个边界 → 第一个边界T extends Number & Comparable → Number

5.4 擦除带来的限制

限制说明变通方案
不能用instanceof检查泛型类型list instanceof List<String> 编译报错只检查原始类型
不能new T()运行时T被擦除为Object传入Class<T>并反射创建
不能new T[]运行时无法确定数组类型使用Array.newInstance()
静态上下文不能使用泛型参数生命周期不匹配使用泛型方法

5.5 补偿机制:桥接方法

为了保持多态,编译器会自动生成桥接方法(Bridge Method)

// 源码
class MyList implements List<String> {
    public void add(String item) { }
}

// 擦除后,编译器生成桥接方法
class MyList implements List {
    public void add(String item) { }      // 用户方法
    public void add(Object item) {        // 桥接方法(自动生成)
        add((String) item);
    }
}

六、泛型和数组

6.1 核心矛盾

数组在运行时必须知道具体类型,泛型在运行时类型被擦除。 → 两者天然冲突。

// ✅ 数组:运行时知道类型
String[] strArray = new String[10];
Integer[] intArray = new Integer[10];
// strArray.getClass() → String[]
// intArray.getClass() → Integer[]

// ❌ 泛型:运行时丢失类型
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// strList.getClass() → ArrayList
// intList.getClass() → ArrayList

6.2 规则:可以声明,不能直接创建java

// ✅ 可以声明带泛型的数组引用
Box<String>[] boxArray;

// ❌ 不能直接创建带泛型的数组
// boxArray = new Box<String>[10]; // 编译报错

// ✅ 可以通过反射创建
boxArray = (Box<String>[]) new Box[10]; // 有警告,但可行

// ✅ 最佳实践:使用 Array.newInstance()
public static <T> T[] createArray(Class<T> clazz, int size) {
    @SuppressWarnings("unchecked")
    T[] array = (T[]) Array.newInstance(clazz, size);
    return array;
}

6.3 为什么Array.newInstance()可以?

因为通过Class<T>对象在运行时获取了具体类型,绕过了泛型擦除的限制。


七、最佳实践与速查表

7.1 泛型使用决策表

场景推荐写法示例
定义一个容器类泛型类class Box<T>
方法的返回值和参数需要类型一致泛型方法<T> T getFirst(List<T>)
只需要读取数据? extends TList<? extends Number>
只需要写入数据? super TList<? super Integer>
既要读又要写不用通配符,用<T>void swap(Box<T>, Box<T>)
完全不关心类型?List<?>

7.2 常见泛型符号

符号含义常用场景
TType(类型)通用类型参数
EElement(元素)集合元素类型
KKey(键)Map的键
VValue(值)Map的值
RResult(结果)返回结果类型
?未知类型通配符

7.3 重要规则速记

  1. 静态上下文不能使用类的泛型参数

  2. 不能创建泛型数组new T[10] 不允许)

  3. 不能instanceof泛型类型

  4. 泛型类型在运行时被擦除

  5. PECS原则:Producer extends,Consumer super

  6. 子类继承父类时,父类的泛型必须"落地"

  7. 钻石操作符<>让类型推断更简洁


八、结语

泛型是Java语言中一个精妙而强大的设计。它通过编译时类型检查保证了类型安全,通过类型擦除实现了向后兼容,通过通配符提供了灵活的类型约束。

理解泛型的核心在于把握两个"对立统一":

  • 类型参数(T 与类型通配符(?:一个用于定义和保持一致性,一个用于使用和增加灵活性

  • 编译时安全运行时擦除:编译时严格的类型检查,运行时零开销的兼容性

掌握泛型,你的Java代码将更加安全、优雅和通用。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐