Skip to content

第18课:泛型

🎯 学习目标

  • 理解泛型的概念和作用
  • 掌握泛型类、泛型方法、泛型接口
  • 理解类型擦除
  • 掌握通配符的使用

📖 一、泛型的概念

为什么需要泛型?

问题:没有泛型时,集合只能存储 Object

java
// JDK 5 之前
List list = new ArrayList();
list.add("Hello");
list.add(123);

String str = (String) list.get(0);  // 需要强制转换
String str2 = (String) list.get(1); // ❌ 运行时错误:ClassCastException

解决:使用泛型

java
// JDK 5+
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123);  // ❌ 编译错误(类型安全)

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

📖 二、泛型类

1. 定义泛型类

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 str = stringBox.get();

Box<Integer> intBox = new Box<>();
intBox.set(123);
int num = intBox.get();

2. 多个类型参数

java
public class Pair<K, V> {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
}

// 使用
Pair<String, Integer> pair = new Pair<>("Age", 25);
String key = pair.getKey();
Integer value = pair.getValue();

📖 三、泛型方法

java
public class GenericMethod {
    // 泛型方法
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
    
    // 使用
    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] strArray = {"A", "B", "C"};
        
        printArray(intArray);  // 1 2 3 4 5
        printArray(strArray);  // A B C
    }
}

📖 四、泛型接口

java
public interface Comparable<T> {
    int compareTo(T o);
}

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

📖 五、类型限定

1. 上界限定(extends)

java
// T 必须是 Number 或其子类
public class NumberBox<T extends Number> {
    private T value;
    
    public void set(T value) {
        this.value = value;
    }
    
    public double doubleValue() {
        return value.doubleValue();  // 可以调用 Number 的方法
    }
}

// 使用
NumberBox<Integer> intBox = new NumberBox<>();     // ✅
NumberBox<Double> doubleBox = new NumberBox<>();   // ✅
// NumberBox<String> strBox = new NumberBox<>();   // ❌ 编译错误

2. 多个限定

java
// T 必须同时是 Comparable 和 Serializable
public class Box<T extends Comparable<T> & Serializable> {
    private T value;
}

📖 六、通配符

1. 无界通配符(?)

java
public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}

// 可以接受任何类型的 List
printList(new ArrayList<String>());
printList(new ArrayList<Integer>());

2. 上界通配符(? extends T)

java
// 只能读取,不能添加(除了 null)
public static double sumOfList(List<? extends Number> list) {
    double sum = 0;
    for (Number num : list) {
        sum += num.doubleValue();
    }
    return sum;
}

// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.0, 2.0, 3.0);
System.out.println(sumOfList(intList));    // 6.0
System.out.println(sumOfList(doubleList)); // 6.0

3. 下界通配符(? super T)

java
// 只能添加,不能读取(只能读取为 Object)
public static void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

// 使用
List<Number> numList = new ArrayList<>();
addNumbers(numList);

4. PECS 原则

Producer Extends, Consumer Super

java
// 从集合读取数据 → 使用 extends
public static void copy(List<? extends Number> src, 
                       List<? super Number> dest) {
    for (Number num : src) {
        dest.add(num);
    }
}

📖 七、类型擦除

1. 什么是类型擦除?

泛型信息只存在于编译期,运行时被擦除。

java
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();

// 运行时都是 ArrayList
System.out.println(list1.getClass() == list2.getClass());  // true

2. 类型擦除的影响

java
// ❌ 不能创建泛型数组
// T[] array = new T[10];  // 编译错误

// ❌ 不能使用 instanceof
// if (obj instanceof List<String>) { }  // 编译错误

// ✅ 可以这样
if (obj instanceof List<?>) { }

💡 最佳实践

  1. 优先使用泛型
java
// ✅ 好
List<String> list = new ArrayList<>();

// ❌ 不好(原始类型)
List list = new ArrayList();
  1. 使用有意义的类型参数名
java
// 常用约定
T - Type(类型)
E - Element(元素)
K - Key(键)
V - Value(值)
N - Number(数字)
  1. PECS 原则
  • 生产者用 extends
  • 消费者用 super

⚠️ 八、常见陷阱

陷阱1:使用原始类型

java
List list = new ArrayList();
list.add("abc");
list.add(123);

原始类型绕过编译期检查,错误会推迟到运行时变成 ClassCastException

陷阱2:误以为 List&lt;String&gt;List&lt;Object&gt; 的子类型

java
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // 编译错误

如果允许这样赋值,就可以通过 objects.add(123) 把 Integer 放进字符串列表,破坏类型安全。

陷阱3:泛型数组

java
// List<String>[] array = new List<String>[10]; // 编译错误

数组在运行时知道元素类型,泛型在运行时被擦除,两者机制冲突。

陷阱4:通配符方向写反

java
List<? extends Number> producer = List.of(1, 2, 3);
// producer.add(4); // 不能安全写入

List<? super Integer> consumer = new ArrayList<Number>();
consumer.add(1); // 可以写入 Integer

记住 PECS:读取用 extends,写入用 super。

陷阱5:运行时判断泛型参数

java
// if (obj instanceof List<String>) { } // 编译错误
if (obj instanceof List<?>) {
    System.out.println("is a list");
}

运行时泛型参数大多被擦除,只能判断 List&lt;?&gt;


🆚 九、Java vs C 对比

特性CJava 泛型
泛型能力通常用宏或 void* 模拟语言级类型参数
类型检查宏弱检查,void* 靠人工约定编译期检查
运行时信息结构体指针无泛型信息类型擦除,大多无泛型实参
容器类型手写或库实现集合框架广泛使用泛型

对 C 程序员来说,Java 泛型可以理解为“编译期类型安全的容器模板”,但它不是 C++ 模板。Java 泛型不会为 List&lt;String&gt;List&lt;Integer&gt; 生成两套运行时代码,而是通过类型擦除复用同一份实现。


📖 十、泛型擦除后的桥接方法

泛型擦除可能生成桥接方法,保证多态仍然成立。

示例:

java
class StringBox implements Comparable<StringBox> {
    @Override
    public int compareTo(StringBox other) {
        return 0;
    }
}

擦除后接口方法接近:

java
int compareTo(Object o);

编译器会生成桥接方法,把 Object 转成 StringBox 后调用真正方法。

这解释了为什么反射查看某些类时,会看到 bridgesynthetic 方法。


📖 十一、泛型 API 设计建议

设计泛型方法时,先判断类型参数是否真的表达约束。

好例子:

java
public static <T> T first(List<T> list) {
    return list.get(0);
}

类型参数 T 同时出现在参数和返回值中,表达“返回值类型与列表元素类型一致”。

坏例子:

java
public static <T> void print(Object value) {
    System.out.println(value);
}

T 没有提供任何约束,应删除。

通配符示例:

java
public static void printAll(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

如果只读取,不关心元素具体类型,List&lt;?&gt;&lt;T&gt; 更直接。


✅ 十二、掌握标准

学完本课后,应能做到:

text
能定义泛型类、泛型接口、泛型方法。
能解释泛型为什么提升类型安全。
能说明类型擦除带来的限制。
能区分 T、E、K、V 等常用命名。
能使用 extends 上界限定。
能正确使用 ?、? extends、? super。
能用 PECS 判断读写方向。
能避免原始类型和泛型数组陷阱。
能理解 Java 泛型与 C 宏/void* 的根本区别。

泛型的核心价值是把类型错误提前到编译期。越靠近公共 API,越应该认真设计泛型边界。


📝 练习

完成 练习/Ex18_Generics.java

  1. 泛型类实现
  2. 泛型方法
  3. 类型限定
  4. 通配符使用
  5. 泛型集合
  6. 综合:通用缓存系统

🎓 下一步

  • 第19课:IO流 - 字节流 - InputStream、OutputStream