第42章

Java泛型

深入学习Java泛型的概念、语法和应用,掌握泛型类、泛型方法、通配符和类型擦除等核心知识

学习目标

什么是泛型

泛型(Generics)是Java 5引入的一个重要特性,它提供了编译时类型安全检测机制,允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

泛型的主要优势

  • 类型安全:在编译时检查类型错误,避免运行时ClassCastException
  • 消除类型转换:避免显式的类型转换,代码更简洁
  • 提高代码重用性:一套代码适用于多种类型
  • 提高性能:避免装箱和拆箱操作
  • 更好的API设计:使API更加清晰和易用
泛型前后对比
// 没有泛型的时代(Java 5之前)
List list = new ArrayList();
list.add("Hello");
list.add(123); // 可以添加任意类型
String str = (String) list.get(0); // 需要强制类型转换

// 使用泛型(Java 5之后)
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译错误,类型不匹配
String str = list.get(0); // 无需类型转换

泛型类型

泛型类

  • 在类名后使用<T>声明类型参数
  • 类型参数可以在类的任何地方使用
  • 支持多个类型参数
  • 可以有有界类型参数
public class Container<T> {
    private T item;
    
    public void setItem(T item) {
        this.item = item;
    }
    
    public T getItem() {
        return item;
    }
}

泛型方法

  • 在方法返回类型前声明<T>
  • 可以在静态方法中使用
  • 独立于类的泛型参数
  • 支持有界类型参数
public static <T> void swap(T[] array, int i, int j) {
    T temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

泛型接口

  • 接口可以声明类型参数
  • 实现类可以指定具体类型
  • 实现类也可以保持泛型
  • 支持多重继承的泛型接口
public interface Comparable<T> {
    int compareTo(T other);
}

public class Person implements Comparable<Person> {
    public int compareTo(Person other) {
        // 比较逻辑
    }
}

泛型通配符

通配符是泛型中的一个重要概念,用问号(?)表示未知类型。通配符主要有三种形式:

通配符类型 语法 说明 使用场景
上界通配符 ? extends T 只能读取,不能写入 生产者(Producer)
下界通配符 ? super T 只能写入,不能读取 消费者(Consumer)
无界通配符 ? 只能进行与类型无关的操作 不确定类型时

PECS原则

Producer Extends, Consumer Super - 这是使用通配符的黄金法则:

  • 如果你需要从集合中读取数据(生产者),使用 ? extends T
  • 如果你需要向集合中写入数据(消费者),使用 ? super T
  • 如果既要读取又要写入,不要使用通配符
通配符使用示例
// 上界通配符 - 只能读取
public static double sum(List<? extends Number> numbers) {
    double total = 0.0;
    for (Number num : numbers) {
        total += num.doubleValue(); // 只能读取
    }
    return total;
}

// 下界通配符 - 只能写入
public static void addNumbers(List<? super Integer> numbers) {
    numbers.add(1); // 只能写入Integer或其子类
    numbers.add(2);
    numbers.add(3);
}

// 无界通配符 - 类型无关操作
public static int getSize(List<?> list) {
    return list.size(); // 只能进行与类型无关的操作
}

有界类型参数

有界类型参数允许你限制类型参数的范围,使用extends关键字来指定类型参数必须是某个类的子类或实现某个接口。

单一边界示例
// 限制T必须是Number或其子类
public class NumberContainer<T extends Number> {
    private T number;
    
    public NumberContainer(T number) {
        this.number = number;
    }
    
    // 可以调用Number类的方法
    public double getDoubleValue() {
        return number.doubleValue();
    }
}
多重边界示例
// T必须同时继承Number并实现Comparable接口
public class ComparableNumber<T extends Number & Comparable<T>> {
    private T value;
    
    public ComparableNumber(T value) {
        this.value = value;
    }
    
    public boolean isGreaterThan(T other) {
        return value.compareTo(other) > 0;
    }
    
    public double getDoubleValue() {
        return value.doubleValue();
    }
}

类型擦除

Java泛型是通过类型擦除来实现的,这意味着泛型信息只存在于编译时,运行时会被擦除。这是Java泛型与C#泛型的主要区别。

类型擦除的影响

  • 运行时无法获取泛型的具体类型信息
  • 不能创建泛型数组:new T[10]
  • 不能实例化类型参数:new T()
  • 不能使用基本类型作为类型参数
  • 不能在静态上下文中使用类型参数
类型擦除演示
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();

// 运行时类型相同,都是ArrayList
System.out.println(stringList.getClass() == integerList.getClass()); // true

// 泛型信息被擦除
System.out.println(stringList.getClass()); // class java.util.ArrayList
System.out.println(integerList.getClass()); // class java.util.ArrayList

完整代码示例

泛型综合应用示例
/**
 * 泛型基础示例
 * 演示泛型类和泛型方法的基本使用
 */
public class GenericBasics {
    
    /**
     * 泛型类示例 - 简单的容器类
     */
    public static class Container<T> {
        private T item;
        
        public Container(T item) {
            this.item = item;
        }
        
        public T getItem() {
            return item;
        }
        
        public void setItem(T item) {
            this.item = item;
        }
        
        public void displayInfo() {
            System.out.println("容器中的项目: " + item);
            System.out.println("项目类型: " + item.getClass().getSimpleName());
        }
    }
    
    /**
     * 泛型方法示例 - 交换数组中两个元素的位置
     */
    public static <T> void swap(T[] array, int i, int j) {
        if (array == null || i < 0 || j < 0 || i >= array.length || j >= array.length) {
            throw new IllegalArgumentException("无效的参数");
        }
        
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    /**
     * 主方法 - 演示泛型的基本使用
     */
    public static void main(String[] args) {
        System.out.println("=== Java泛型基础示例 ===");
        
        // 1. 泛型类的使用
        Container<String> stringContainer = new Container<>("Hello, Generics!");
        stringContainer.displayInfo();
        
        Container<Integer> intContainer = new Container<>(42);
        intContainer.displayInfo();
        
        // 2. 泛型方法的使用
        String[] names = {"Alice", "Bob", "Charlie", "David"};
        System.out.println("交换前: " + Arrays.toString(names));
        swap(names, 0, 3);
        System.out.println("交换后: " + Arrays.toString(names));
    }
}
💻 查看完整代码 - 在线IDE体验

泛型最佳实践

泛型使用原则

推荐做法

// 使用泛型提高类型安全
List<String> names = new ArrayList<>();

// 遵循PECS原则
public void processNumbers(List<? extends Number> numbers) {
    // 只读取数据
}

public void addIntegers(List<? super Integer> numbers) {
    // 只写入数据
}

// 使用有界类型参数
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}
  • 优先使用泛型而不是原始类型
  • 遵循PECS原则使用通配符
  • 使用有界类型参数限制类型范围
  • 泛型方法优于泛型类

避免的做法

// 使用原始类型
List list = new ArrayList(); // 避免

// 不必要的通配符
List<?> list = new ArrayList<String>(); // 过度使用

// 忽略编译器警告
@SuppressWarnings("unchecked")
List<String> list = (List<String>) rawList; // 危险
  • 避免使用原始类型
  • 不要忽略编译器的泛型警告
  • 避免不必要的通配符使用
  • 不要在新代码中混用泛型和原始类型

常见问题解答

Q: 什么时候使用泛型?

A: 当你需要类型安全、避免类型转换、提高代码重用性时。特别是在集合、工具类、框架开发中。

Q: extends和super的区别?

A: extends用于读取(生产者),super用于写入(消费者)。记住PECS原则:Producer Extends, Consumer Super。

Q: 为什么不能创建泛型数组?

A: 由于类型擦除,运行时无法确定数组的确切类型,可能导致类型安全问题。可以使用List代替数组,或者使用反射创建。

本章小结