Java笔记 —— 泛型

在Java开发中,泛型(Generics)是一项革命性的特性,它于JDK 5引入,让代码在编译时就能捕获类型错误,并消除了大量繁琐的类型转换。本文将带你全面了解Java泛型的原理、使用场景和最佳实践。

一、为什么需要泛型?

在没有泛型的年代,我们只能这样写集合:

java 复制代码
List list = new ArrayList();
list.add("hello");
list.add(123);   // 随意添加不同类型
String s = (String) list.get(0); // 需要强制转换,容易出错

这种方式存在两个问题:

  1. 类型不安全:可以向集合中添加任意类型,编译时无法检查。

  2. 代码冗长 :取值时需要进行强制类型转换,而且转换失败会在运行时抛出ClassCastException

泛型解决了这两个问题,它提供了编译时类型安全消除了强制转换

java 复制代码
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译错误,类型不匹配
String s = list.get(0); // 无需转换

二、泛型的基本概念

2.1 类型参数

泛型中的类型参数通常用单个大写字母表示,常见的有:

  • E - Element(元素,常用于集合)

  • K - Key(键)

  • V - Value(值)

  • N - Number(数字)

  • T - Type(类型)

  • S,U,V - 第二、三、四个类型参数

2.2 泛型类

定义一个泛型类,在类名后加上尖括号<>

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

使用:

java 复制代码
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String str = stringBox.get();

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

2.3 泛型接口

接口也可以声明类型参数:

java 复制代码
public interface Pair<K, V> {
    K getKey();
    V getValue();
}

实现时指定具体类型:

java 复制代码
public class OrderedPair<K, V> implements Pair<K, V> {
    private K key;
    private V value;
    
    public OrderedPair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    @Override
    public K getKey() { return key; }
    
    @Override
    public V getValue() { return value; }
}

2.4 泛型方法

方法也可以声明自己的类型参数,独立于类:

java 复制代码
public class Util {
    // 泛型方法,<T>放在返回值之前
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }
    
    // 更复杂的例子:比较两个Pair
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

调用时,类型参数会被自动推断:

java 复制代码
String middle = Util.getMiddle("a", "b", "c");
Integer midNum = Util.getMiddle(1, 2, 3);

三、类型边界

有时我们需要限制类型参数的范围,例如要求类型参数必须是某个类的子类或实现某个接口。

3.1 上界通配符

使用extends关键字设置上界:

java 复制代码
// 只接受Number及其子类
public class NumberBox<T extends Number> {
    private T number;
    
    public double doubleValue() {
        return number.doubleValue();
    }
}

可以指定多个边界,用&连接:

java 复制代码
public class MultipleBound<T extends Number & Comparable<T>> {
    // T必须同时是Number的子类并实现Comparable接口
}

3.2 泛型方法中的类型边界

java 复制代码
// 计算数组中的最小值,T必须实现Comparable
public static <T extends Comparable<T>> T min(T[] array) {
    if (array == null || array.length == 0) return null;
    T smallest = array[0];
    for (int i = 1; i < array.length; i++) {
        if (smallest.compareTo(array[i]) > 0) {
            smallest = array[i];
        }
    }
    return smallest;
}

四、通配符(Wildcard)

通配符?表示未知类型,常用于方法的参数中,使方法更灵活。

4.1 无界通配符

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

List<?>表示任何类型的List,但不能向其中添加元素(除了null),因为类型未知。

4.2 上界通配符

java 复制代码
// 接受任何Number及其子类的List
public double sum(List<? extends Number> list) {
    double sum = 0.0;
    for (Number num : list) {
        sum += num.doubleValue();
    }
    return sum;
}

? extends T表示T或T的子类。这种形式适用于读取场景,因为你知道元素至少是T类型。

4.3 下界通配符

java 复制代码
// 向列表中添加Integer或其父类对象
public void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);   // 可以添加Integer
    }
    // Object obj = list.get(0); // 可以获取,但类型是Object
}

? super T表示T或T的父类。这种形式适用于写入场景,因为你知道可以安全地添加T类型及其子类型。

4.4 PECS原则

PECS(Producer Extends, Consumer Super)是通配符使用的经典原则:

  • Producer Extends :如果参数化类型代表一个生产者 (提供数据),使用<? extends T>

  • Consumer Super :如果代表一个消费者 (消费数据),使用<? super T>

java 复制代码
// 生产者:从集合中读取数据
public void copy(List<? extends Number> src, List<? super Number> dest) {
    for (Number num : src) {
        dest.add(num);
    }
}

五、类型擦除(Type Erasure)

Java泛型是通过类型擦除实现的,这意味着泛型信息只在编译时存在,运行时会被移除。

5.1 什么是类型擦除?

编译器在编译时进行类型检查,然后将泛型类型替换为原始类型(Raw Type),并插入必要的强制转换。

例如:

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

编译后实际等价于:

java 复制代码
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

5.2 擦除的规则

  • 无限定类型参数(如<T>)被替换为Object

  • 有上界的类型参数(如<T extends Number>)被替换为第一个边界类型(这里是Number)。

  • 泛型方法也会被擦除,并可能生成桥方法(Bridge Method)以保持多态。

5.3 擦除带来的限制

由于擦除,泛型有一些使用限制:

1. 不能实例化类型参数

java 复制代码
// 错误
T obj = new T();
// 错误
T[] array = new T[10];

2. 静态上下文中不能引用类型参数

java 复制代码
public class Box<T> {
    // 错误:静态成员不能使用类的类型参数
    private static T value;
    
    // 错误:静态方法不能使用类的类型参数
    public static T getValue() { ... }
}

3. 不能创建泛型数组

java 复制代码
// 编译错误
List<String>[] array = new List<String>[10];
// 可以这样绕过,但不安全
List<String>[] array = (List<String>[]) new List[10];

4. 不能使用instanceof判断泛型类型

java 复制代码
// 错误
if (obj instanceof List<String>) { ... }
// 只能判断原始类型
if (obj instanceof List<?>) { ... }

六、泛型与继承

泛型类型与继承的关系需要特别注意:

  • List<String>不是List<Object>的子类型。

  • List<Integer>不是List<Number>的子类型,尽管IntegerNumber的子类型。

  • 数组是协变的(covariant),而泛型是不变的(invariant)。

java 复制代码
// 数组协变:允许
Number[] numbers = new Integer[10];
// 泛型不变:不允许
List<Number> list = new ArrayList<Integer>(); // 编译错误

这正是通配符发挥作用的地方:

java 复制代码
// 使用上界通配符实现类似协变
List<? extends Number> list = new ArrayList<Integer>();

七、泛型的最佳实践

7.1 优先使用泛型,避免原始类型

永远不要使用原始类型(如List而不是List<String>),这会失去类型安全性。

7.2 使用通配符增加API灵活性

在设计方法参数时,合理使用? extends? super让API更通用。

7.3 优先考虑使用泛型方法而不是通配符作为返回值

java 复制代码
// 好的做法:使用泛型方法
public static <T> T getFirst(List<T> list) { ... }

// 不推荐:通配符作为返回值,调用者需要强制转换
public static <?> getFirst(List<?> list) { ... }

7.4 类型参数命名要有意义

对于简单情况,单字母可以接受;但复杂场景下,使用有意义的名称(如KeyTypeValueType)提高可读性。

7.5 避免泛型数组

尽量使用集合代替数组,或者使用List来达到类似目的。

八、Java 7+的改进

  • 菱形操作符:Java 7开始,实例化泛型类时可以省略类型参数:

    java 复制代码
    List<String> list = new ArrayList<>(); // 菱形操作符
  • 局部变量类型推断 :Java 10引入var,可以简化局部变量声明:

    java 复制代码
    var list = new ArrayList<String>(); // 类型推断为ArrayList<String>

九、总结

Java泛型是一个强大且复杂的特性,它通过编译时类型检查提供了类型安全,并消除了强制转换的繁琐。理解泛型需要掌握:

  • 泛型类、接口、方法的定义与使用

  • 类型边界的约束

  • 通配符的灵活运用及PECS原则

  • 类型擦除的原理及其带来的限制

相关推荐
未知鱼2 小时前
Python安全开发之简易whois查询
java·python·安全
Aloha_up2 小时前
spring的几个八股
java·后端·spring
逸Y 仙X2 小时前
文章九:ElasticSearch索引字段常见属性
java·大数据·服务器·数据库·elasticsearch·搜索引擎
左左右右左右摇晃2 小时前
Java笔记——多态
java·笔记·python
空空潍2 小时前
2026年IDEA、PyCharm等专业版学生免费申请教育许可证
java·ide·intellij-idea
24白菜头2 小时前
若依框架Ruoyi-Vue-SpringBoot3部署
前端·javascript·笔记·后端·学习
MX_93592 小时前
基于注解方式配置声明式事务
java·开发语言·后端·spring
Java面试题总结2 小时前
Spring AI 初步集成(2)-添加记忆
java·人工智能·spring
野犬寒鸦2 小时前
JVM垃圾回收机制深度解析(G1篇)(垃圾回收过程及专业名词详解)
java·服务器·jvm·后端·面试