【Java SE】泛型(Generics)

泛型

在Java开发中,泛型(Generics)是一个看似简单却蕴含深意的特性。自JDK 5引入以来,它已经成为Java类型系统的重要组成部分。

为什么需要泛型?

泛型出现之前的问题

在泛型出现之前,Java集合类(如ArrayList)存储的是Object类型,这意味着可以向集合中添加任何类型的对象。

java 复制代码
List list = new ArrayList();
list.add("Hello");
list.add(123); // 可以添加Integer,但容易引发问题

// 取出时需要强制转换
String str = (String) list.get(0); // 没问题
String error = (String) list.get(1); // 运行时抛出 ClassCastException

这段代码在编译时完全正常,但在运行时却会崩溃。问题的根源在于:编译器无法知道集合中元素的实际类型,类型安全只能靠程序员手动保证

泛型的解决方案

泛型提供了编译时类型安全检查机制,在定义类、接口、方法时可以指定类型参数。

java 复制代码
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译错误:不兼容的类型

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

通过泛型,类型错误在编译阶段就被发现了,代码也更加清晰易读。

泛型基础

泛型类

定义一个泛型类,使用尖括号<>声明类型参数,通常使用单个大写字母:

类型参数 含义 使用场景
T Type 表示任意类型,最常用。当只有一个类型参数时,通常使用T
E Element 表示集合中的元素类型,如List<E>Set<E>
K Key 表示映射中的键类型,如Map<K, V>
V Value 表示映射中的值类型,如Map<K, V>
N Number 表示数值类型
S, U, V 第二、三、四个类型参数 当需要多个类型参数时的扩展
R Return Type 表示方法的返回类型(常见于函数式编程)
java 复制代码
public class Box<T> {
    private T content;
    
    public void set(T content) {
        this.content = content;
    }
    
    public T get() {
        return content;
    }
    
    public static void main(String[] args) {
        Box<String> stringBox = new Box<String>();
        //Box<String> stringBox = new Box<>();省略后面的String也可以
        
        stringBox.set("Hello");
        String value = stringBox.get(); // 类型安全,无需转换
        
        Box<Integer> intBox = new Box<>();//Box<Integer> intBox = new Box<Integer>();
        intBox.set(123);
    }
}

泛型接口

接口也可以声明泛型,最典型的例子就是ListSetMap等集合接口:

java 复制代码
public interface Generator<T> {
    T next();
}

public class NumberGenerator implements Generator<Integer> {
    private int count = 0;
    
    @Override
    public Integer next() {
        return count++;
    }
}

泛型方法

泛型方法可以定义在普通类中,也可以定义在泛型类中。类型参数放在返回类型之前:

java 复制代码
public class GenericMethodExample {
    
    // 泛型方法,定义类型参数 T
    public static <T> T getMiddle(T... arr) {
        return arr[arr.length / 2];
    }
    
    public static void main(String[] args) {
        String middleStr = getMiddle("a", "b", "c"); // T 被推断为 String
        Integer middleInt = getMiddle(1, 2, 3, 4);   // T 被推断为 Integer
    }
}

类型边界

有时我们需要限制类型参数的范围,比如只允许某个类的子类。这时可以使用extends关键字定义上界:

java 复制代码
// 只允许 Number 及其子类
public class NumberBox<T extends Number> {
    private T number;
    
    public double doubleValue() {
        return number.doubleValue(); // 可以调用 Number 类的方法
    }
    
    public void set(T number) {
        this.number = number;
    }
}

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

多个边界

Java支持多个边界,使用&连接:

java 复制代码
// T 必须同时是 Serializable 和 Comparable 的子类型
public class MultiBound<T extends Serializable & Comparable<T>> {
    // ...
}

通配符(Wildcard)

通配符?是泛型中一个非常强大的概念,它解决了泛型类型之间的协变和逆变问题。

为什么需要通配符?

先看一个常见的困惑:List<String>List<Object> 的子类型吗?

java 复制代码
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // 编译错误!

答案是不是 。如果允许这样做,我们就可以向strings中添加非String类型的对象,破坏了类型安全。

那么,如何表示一个可以接受任何List的方法呢?这就引出了无界通配符

无界通配符 <?>

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

// 可以传入任何类型的 List
List<String> names = Arrays.asList("Alice", "Bob");
List<Integer> numbers = Arrays.asList(1, 2, 3);
printList(names);   // OK
printList(numbers); // OK

注意:List<?> 是只读的(除了添加null),因为你不知道具体的类型。

上界通配符 <? extends T>

需要从集合中读取元素时,使用上界通配符。它表示类型是TT的子类。

java 复制代码
public double sumOfList(List<? extends Number> list) {
    double sum = 0.0;
    for (Number num : list) {
        sum += num.doubleValue(); // 可以安全地读取为 Number
    }
    return sum;
}

// 使用
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2);
sumOfList(ints);    // OK
sumOfList(doubles); // OK

PECS原则(Producer Extends, Consumer Super) :如果需要从集合中读取数据(生产者),使用extends

下界通配符 <? super T>

需要向集合中写入元素时,使用下界通配符。它表示类型是TT的超类。

java 复制代码
public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
    // 可以安全地添加 Integer
}

// 使用
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // OK
addNumbers(objects); // OK
// List<Integer> ints = new ArrayList<>(); // 也能用,但注意语义

PECS原则 :如果需要向集合中写入数据(消费者),使用super

PECS 完整记忆法

PECS: Producer Extends, Consumer Super

  • Producer (生产者) :如果你从一个数据结构中获取元素,它是生产者,使用 <? extends T>
  • Consumer (消费者) :如果你向一个数据结构中放入元素,它是消费者,使用 <? super T>
java 复制代码
// 典型例子:Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    // src 是生产者,dest 是消费者
}

类型擦除

泛型是Java在编译时的语法糖,在运行时,泛型类型信息会被擦除。这是Java泛型与C#模板的核心区别。

什么是类型擦除?

编译器在编译时会:

  1. 将泛型代码替换为原始类型(Raw Type)
  2. 自动插入必要的强制类型转换
  3. 生成桥接方法以保持多态性
java 复制代码
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);

// 编译后(大致等价)
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0); // 自动插入强制转换

类型擦除代码验证

java 复制代码
MyArray<Integer> myArray = new MyArray<>();

MyArray<String> myArray2 = new MyArray<>();

System.out.println(myArray);

System.out.println(myArray2);

输出结果

复制代码
MyArray@1b6d3586
MyArray@4554617c

类型擦除带来的限制

1. 运行时无法区分泛型类型

java 复制代码
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true,都是 ArrayList

2. 不能使用 instanceof 检查泛型类型

java 复制代码
if (list instanceof List<String>) // 编译错误

3. 不能创建泛型数组

java 复制代码
T[] array = new T[10]; // 编译错误
// 可以这样绕过:
List<T>[] array = (List<T>[]) new List[10]; // 但会有警告

4. 静态上下文中不能使用泛型类型参数

java 复制代码
public class GenericClass<T> {
    private static T instance; // 编译错误
    public static T getInstance() { ... } // 编译错误
}

泛型在实际开发中的最佳实践

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

java 复制代码
// 不推荐:原始类型
List list = new ArrayList();

// 推荐:泛型
List<String> list = new ArrayList<>();

使用泛型方法替代通配符(当类型参数有关联时)

java 复制代码
// 使用通配符
public void swap(List<?> list, int i, int j) {
    // 无法直接实现,因为不能从 List<?> 中取出元素再放回
}

// 使用泛型方法
public <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);
}

复杂场景应用:构建类型安全的 Builder 模式

java 复制代码
public class Person {
    private String name;
    private Integer age;
    
    private Person(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
    }
    
    public static Builder builder() {
        return new Builder();
    }
    
    public static class Builder {
        private String name;
        private Integer age;
        
        public Builder name(String name) {
            this.name = name;
            return this;
        }
        
        public Builder age(Integer age) {
            this.age = age;
            return this;
        }
        
        public Person build() {
            // 可以添加校验逻辑
            if (name == null || age == null) {
                throw new IllegalStateException("name and age are required");
            }
            return new Person(this);
        }
    }
}

常见陷阱与注意事项

不能使用基本类型作为类型参数

java 复制代码
List<int> list = new ArrayList<>(); // 编译错误
List<Integer> list = new ArrayList<>(); // 使用包装类

泛型方法中的类型推断

java 复制代码
// 泛型方法
public static <T> T getValue(T t) {
    return t;
}

// 调用时可以显式指定类型
String s = GenericClass.<String>getValue("hello");
// 也可以让编译器推断
String s = getValue("hello");

可变参数与泛型

Java 在可变参数中使用泛型时会产生堆污染警告,可以使用 @SafeVarargs 注解抑制:

java 复制代码
@SafeVarargs
public static <T> List<T> asList(T... elements) {
    return Arrays.asList(elements);
}
相关推荐
ZTLJQ3 小时前
数据的另一面:Python中NoSQL数据库完全解析
开发语言·python·nosql
烧饼Fighting3 小时前
java+vue推rtsp流实现视频播放(由javacv+ffmpg转为vlcj)
java·开发语言·音视频
Predestination王瀞潞3 小时前
Base Tools-Associate-Second:CSV库详解
python·csv
紫丁香3 小时前
03-Flask请求上下文响应与错误处理机制深度解析
后端·python·flask
云霄IT3 小时前
安卓apk逆向之crc32检测打补丁包crc32_patcher.py
java·前端·python
小句3 小时前
Java Web 技术演进:Servlet → Spring → Spring Boot
java·前端·spring
kyle~3 小时前
JNI与JNA ---打通Java服务端与C++机器人系统的通信链路
java·c++·机器人
极光代码工作室3 小时前
基于深度学习的中文文本情感分析系统
人工智能·python·深度学习·神经网络·nlp
XiYang-DING3 小时前
【Java SE】缓存池和常量池的区别
java·spring·缓存