网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

泛型 ​

字数: 0 字 时长: 0 分钟

泛型(Generics)是 Java 5 引入的重要机制。

泛型的核心思想可以概括为:

把类型作为参数,在编译阶段对类型进行约束和检查。

例如:

java
ArrayList<String> list =
        new ArrayList<>();

list.add("周芷若");

String name =
        list.get(0);

这里:

java
<String>

表示这个集合只能按照:

text
String

类型来使用。

泛型主要解决三个问题:

text
类型安全
    ↓
编译阶段发现类型错误


减少强制类型转换
    ↓
取出数据时直接得到目标类型


提高代码复用性
    ↓
一套代码可以处理多种类型

泛型在 Java 中应用非常广泛,例如:

text
List<E>

Set<E>

Map<K, V>

Optional<T>

Comparable<T>

Future<T>

Callable<V>

集合框架只是泛型最典型的应用场景之一,泛型本身应该作为一个独立的 Java 基础知识来理解。

1. 为什么需要泛型 ​

在没有泛型的时候,可以使用:

java
Object

保存任意对象。

例如自己实现一个简单容器:

java
class Arraylist {

    private Object[] elements;

    private int size;

    public Arraylist(
            int initialCapacity) {

        elements =
                new Object[
                        initialCapacity
                ];
    }

    public void add(
            Object object) {

        elements[size++] =
                object;
    }

    public Object get(
            int index) {

        return elements[index];
    }
}

使用:

java
Arraylist list =
        new Arraylist(10);

list.add("周芷若");

list.add(new Date());

由于:

java
Object

可以接收几乎所有对象,所以一个容器中可能混入不同类型的数据。

取出数据时还需要:

java
String name =
        (String) list.get(0);

进行强制类型转换。

这会带来两个明显的问题。

1.1 类型不安全 ​

例如:

java
list.add("周芷若");

list.add(new Date());

编译器无法阻止:

text
String

Date

被同时放进容器。

如果错误地转换:

java
String value =
        (String) list.get(1);

程序在运行时才会抛出:

text
ClassCastException

1.2 需要大量强制类型转换 ​

因为:

java
get()

返回的是:

java
Object

所以每次取数据都需要:

java
(String)

(Date)

(User)

等强制转换。

泛型就是为了解决这类问题。

使用:

java
ArrayList<String> list =
        new ArrayList<>();

以后:

java
list.add("周芷若");

合法。

但是:

java
// 编译错误
// list.add(new Date());

编译器会直接阻止错误类型进入集合。

取出数据:

java
String value =
        list.get(0);

也不再需要显式强制转换。

所以:

text
没有泛型
    ↓
Object
    ↓
什么都能放
    ↓
运行时才可能发现错误


使用泛型
    ↓
类型参数
    ↓
编译阶段约束类型
    ↓
更安全

2. 泛型的本质:参数化类型 ​

泛型的本质是:

text
参数化类型
Parameterized Type

普通方法可以把:

text
数据

作为参数:

java
sum(10, 20);

泛型则可以把:

text
类型

参数化。

例如:

java
Box<String>

这里可以理解为:

text
Box
    ↓
类型参数 T
    ↓
实际传入 String

定义:

java
public class Box<T> {

    private T value;

    public Box(T value) {

        this.value = value;
    }

    public T getValue() {

        return value;
    }

    public void setValue(
            T value) {

        this.value = value;
    }
}

使用:

java
Box<String> stringBox =
        new Box<>("Java");

Box<Integer> integerBox =
        new Box<>(100);

同一个:

java
Box<T>

既可以用于:

text
String

也可以用于:

text
Integer

这就是泛型带来的代码复用。

3. 泛型类 ​

在类名后面使用:

java
<T>

就可以定义泛型类。

基本语法:

java
class 类名<T> {
}

例如:

java
public class Box<T> {

    private T value;

    public T getValue() {

        return value;
    }

    public void setValue(
            T value) {

        this.value = value;
    }
}

这里:

text
T

就是:

text
类型参数
Type Parameter

使用时再传入具体类型:

java
Box<String> box =
        new Box<>();

box.setValue("kerwim");

String value =
        box.getValue();

也可以:

java
Box<Integer> box =
        new Box<>();

box.setValue(18);

Integer value =
        box.getValue();

3.1 自己实现一个泛型容器 ​

把之前基于:

java
Object

的容器改成泛型:

java
class Arraylist<E> {

    private Object[] elementData;

    private int size;

    public Arraylist(
            int initialCapacity) {

        elementData =
                new Object[
                        initialCapacity
                ];
    }

    public boolean add(
            E element) {

        elementData[size++] =
                element;

        return true;
    }

    @SuppressWarnings(
            "unchecked"
    )
    public E get(
            int index) {

        return (E)
                elementData[index];
    }
}

这里:

text
E

代表:

text
Element

也就是元素类型。

使用字符串:

java
Arraylist<String> list =
        new Arraylist<>(4);

list.add("灭");

list.add("绝");

String value =
        list.get(0);

使用日期:

java
Arraylist<Date> list =
        new Arraylist<>(4);

list.add(
        new Date()
);

Date date =
        list.get(0);

同一个:

java
Arraylist<E>

可以根据使用者传入的类型参数处理不同数据。

4. 泛型类型参数命名 ​

泛型类型参数理论上可以使用任意合法变量名:

java
class Box<ABC> {
}

语法没有问题。

但 Java 社区通常使用一些约定俗成的单字母名称:

类型参数常见含义
TType,类型
EElement,元素
KKey,键
VValue,值
NNumber,数字
RResult / Return,结果
S、U第二、第三个类型参数

例如:

java
Map<K, V>

表示:

text
K → Key

V → Value

多个类型参数可以这样定义:

java
public class Pair<K, V> {

    private K key;

    private V value;
}

使用:

java
Pair<String, Integer> pair =
        new Pair<>();

5. 菱形操作符 ​

早期泛型代码经常写成:

java
ArrayList<String> list =
        new ArrayList<String>();

从 Java 7 开始,可以使用:

text
<>

让编译器推断右侧类型:

java
ArrayList<String> list =
        new ArrayList<>();

这个:

java
<>

通常称为:

text
菱形操作符
Diamond Operator

现在实际开发一般都使用:

java
new ArrayList<>();

而不会重复写:

java
new ArrayList<String>();

6. 泛型方法 ​

泛型不仅可以定义在类上,也可以单独定义在方法上。

基本语法:

java
public <T> T method(
        T value) {
}

需要注意:

text
<T>

写在:

text
返回值类型之前

例如:

java
public static <T> T echo(
        T value) {

    return value;
}

调用:

java
String name =
        echo("kerwim");

Integer age =
        echo(18);

编译器可以根据参数推断:

text
T

到底是什么类型。

6.1 泛型类和泛型方法不是一回事 ​

例如:

java
class Box<E> {

    public <T> T convert(
            T value) {

        return value;
    }
}

这里存在两个完全独立的类型参数:

text
E
    ↓
Box 类的类型参数


T
    ↓
convert() 方法自己的类型参数

例如:

java
Box<String> box =
        new Box<>();

Integer value =
        box.convert(100);

即使:

text
Box<E>

中的:

text
E = String

也完全不影响:

text
convert()

中的:

text
T = Integer

6.2 泛型方法示例 ​

类似 ArrayList.toArray() 的思路:

java
class Arraylist<E> {

    private Object[] elementData;

    private int size;

    public <T> T[] toArray(
            T[] array) {

        return Arrays.copyOf(
                elementData,
                size,
                (Class<? extends T[]>)
                        array.getClass()
        );
    }
}

泛型方法的基本结构可以参考:

image-20250305205123796

调用:

java
Arraylist<String> list =
        new Arraylist<>(4);

list.add("灭");

list.add("绝");

list.add("师");

list.add("太");

String[] values =
        new String[4];

values =
        list.toArray(values);

for (String value : values) {

    System.out.println(value);
}

7. 泛型接口 ​

接口同样可以使用泛型。

例如:

java
public interface Converter<F, T> {

    T convert(F source);
}

实现:

java
public class StringToIntegerConverter
        implements Converter<
                String,
                Integer> {

    @Override
    public Integer convert(
            String source) {

        return Integer.valueOf(
                source
        );
    }
}

调用:

java
Converter<String, Integer>
        converter =
        new StringToIntegerConverter();

Integer value =
        converter.convert("100");

实际项目中的:

text
Comparator<T>

Comparable<T>

Callable<V>

都是典型的泛型接口。

8. 泛型上界 extends ​

有时候我们并不希望:

java
<T>

可以代表任意类型。

例如:

只允许 Number 以及它的子类。

可以使用:

java
<T extends Number>

这种语法称为:

text
上界
Upper Bound

例如:

java
public class NumberBox<
        T extends Number> {

    private T value;

    public NumberBox(
            T value) {

        this.value = value;
    }

    public double doubleValue() {

        return value.doubleValue();
    }
}

允许:

java
NumberBox<Integer> intBox =
        new NumberBox<>(10);

NumberBox<Double> doubleBox =
        new NumberBox<>(10.5);

但是:

java
// 编译错误
// NumberBox<String> box =
//         new NumberBox<>("Java");

因为:

text
String

不是:

text
Number

或者其子类。

8.1 原来的继承例子 ​

假设:

java
class MieJueShiTai {

    @Override
    public String toString() {

        return "灭绝师太";
    }
}
java
class ZhouZhiRuo
        extends MieJueShiTai {

    @Override
    public String toString() {

        return "周芷若";
    }
}
java
class ZhangWuJi
        extends ZhouZhiRuo {

    @Override
    public String toString() {

        return "张无忌";
    }
}

如果定义:

java
class Arraylist<
        E extends ZhouZhiRuo> {
}

那么:

java
Arraylist<ZhouZhiRuo> list1 =
        new Arraylist<>();

Arraylist<ZhangWuJi> list2 =
        new Arraylist<>();

合法。

但是:

java
// 编译错误
// Arraylist<MieJueShiTai> list =
//         new Arraylist<>();

因为:

text
MieJueShiTai

是:

text
ZhouZhiRuo

的父类,而不是它的子类。

所以:

java
<E extends ZhouZhiRuo>

表示:

text
E 的最大范围是 ZhouZhiRuo
    ↓
允许 ZhouZhiRuo
    ↓
以及 ZhouZhiRuo 的子类

9. 多重上界 ​

泛型参数还可以同时受到多个条件约束。

例如:

java
<T extends Number
        & Comparable<T>>

表示:

text
T 必须是 Number 的子类

并且

T 必须实现 Comparable<T>

例如:

java
public static <
        T extends Number
                & Comparable<T>>
T max(
        T a,
        T b) {

    return a.compareTo(b) >= 0
            ? a
            : b;
}

需要注意:

如果同时存在类和接口,类必须写在最前面。

例如:

java
<T extends Number
        & Comparable<T>>

而不能写成:

java
// 错误
// <T extends Comparable<T>
//         & Number>

10. 泛型具有不变性 ​

这一点非常重要。

虽然:

text
Integer is a Number

但是:

text
List<Integer>

并不是:

text
List<Number>

的子类型。

例如:

java
List<Integer> integers =
        new ArrayList<>();

// 编译错误
// List<Number> numbers =
//         integers;

为什么?

假设这种赋值允许:

java
List<Integer> integers =
        new ArrayList<>();

List<Number> numbers =
        integers;

那么通过:

java
numbers

就可以:

java
numbers.add(3.14);

但原来的对象实际上是:

java
List<Integer>

这样集合中就混入了:

text
Double

破坏了类型安全。

所以 Java 泛型默认具有:

text
不变性
Invariance

也就是:

text
A 是 B 的子类

并不代表:

text
Generic<A>

是:

text
Generic<B>

的子类。

这也是为什么泛型需要:

text
通配符
Wildcard

11. 泛型通配符 ? ​

通配符使用:

java
?

表示:

text
未知类型

例如:

java
List<?>

表示:

一个元素具体类型未知的 List。

它可能实际是:

text
List<String>

List<Integer>

List<User>

所以:

java
public static void printList(
        List<?> list) {

    for (Object element : list) {

        System.out.println(
                element
        );
    }
}

可以接受:

java
List<String> strings =
        List.of(
                "Java",
                "Spring"
        );

List<Integer> numbers =
        List.of(
                1,
                2,
                3
        );

printList(strings);

printList(numbers);

11.1 List<?> 能不能添加数据 ​

对于:

java
List<?> list

编译器不知道:

text
?

究竟是什么类型。

所以不能随便:

java
list.add("Java");

list.add(100);

因为:

text
List<?>

可能实际指向:

text
List<Integer>

也可能指向:

text
List<String>

唯一总是安全的值通常是:

java
null

读取时可以安全得到:

java
Object value =
        list.get(0);

所以:

text
List<?>
    ↓
可以安全读取为 Object
    ↓
不能随便添加具体对象

12. 上限通配符 ? extends T ​

语法:

java
<? extends T>

表示:

未知类型,但它一定是 T 或 T 的子类型。

例如:

java
List<? extends Number>

它可能指向:

text
List<Integer>

List<Double>

List<Long>

例如:

java
public static double sum(
        List<? extends Number>
                numbers) {

    double result = 0;

    for (Number number :
            numbers) {

        result +=
                number.doubleValue();
    }

    return result;
}

可以传入:

java
List<Integer> integers =
        List.of(
                1,
                2,
                3
        );

List<Double> doubles =
        List.of(
                1.1,
                2.2
        );

sum(integers);

sum(doubles);

12.1 为什么不能随便 add ​

例如:

java
List<? extends Number> list;

它可能实际引用的是:

java
List<Integer>

也可能是:

java
List<Double>

所以编译器不能确定:

java
list.add(
        Integer.valueOf(1)
);

是否安全。

因为如果实际是:

java
List<Double>

就出问题了。

所以:

java
List<? extends Number>

通常:

text
适合读取

不适合写入

读取:

java
Number number =
        list.get(0);

是安全的。

13. 下限通配符 ? super T ​

语法:

java
<? super T>

表示:

未知类型,但一定是 T 或 T 的父类型。

例如:

java
List<? super Integer>

可能实际是:

text
List<Integer>

List<Number>

List<Object>

因此可以安全添加:

java
Integer

对象。

例如:

java
public static void addNumbers(
        List<? super Integer>
                list) {

    list.add(10);

    list.add(20);
}

下面都可以:

java
List<Integer> integers =
        new ArrayList<>();

List<Number> numbers =
        new ArrayList<>();

List<Object> objects =
        new ArrayList<>();

addNumbers(integers);

addNumbers(numbers);

addNumbers(objects);

13.1 为什么读取只能得到 Object ​

对于:

java
List<? super Integer> list

编译器只知道:

text
实际类型可能是 Integer 的某个父类型

但不知道到底是:

text
Integer

Number

Object

所以读取时只能安全认为:

java
Object value =
        list.get(0);

不能直接:

java
// 编译错误
// Integer value =
//         list.get(0);

因此:

text
? extends T
    ↓
适合读


? super T
    ↓
适合写

14. PECS 原则 ​

理解泛型通配符时,有一个非常经典的原则:

text
PECS

全称:

text
Producer Extends

Consumer Super

可以理解为:

text
如果一个泛型结构主要负责提供数据
    ↓
使用 ? extends T


如果一个泛型结构主要负责接收数据
    ↓
使用 ? super T

例如复制数据:

java
public static <T> void copy(
        List<? extends T> source,
        List<? super T> target) {

    for (T element : source) {

        target.add(element);
    }
}

这里:

text
source
    ↓
生产数据
    ↓
? extends T


target
    ↓
接收数据
    ↓
? super T

可以直接记成:

只读倾向 extends,只写倾向 super。

当然这里的:

text
只读 / 只写

是帮助理解的简化说法,并不是指集合真的失去了其他全部操作。

15. extends 在泛型中的两种用途 ​

这里非常容易混淆。

15.1 类型参数上界 ​

例如:

java
<T extends Number>

这里是在:

text
定义类型参数 T

意思是:

text
T 必须是 Number 或其子类型

15.2 通配符上界 ​

例如:

java
List<? extends Number>

这里的:

text
?

代表一个未知类型。

意思是:

text
某种 Number 或 Number 子类型的 List

两者虽然都使用:

java
extends

但意义不同:

text
<T extends Number>
    ↓
声明一个有上界的类型参数


<? extends Number>
    ↓
声明一个有上界的未知类型

16. 泛型类型擦除 ​

Java 泛型采用:

text
类型擦除
Type Erasure

实现。

简单理解:

泛型主要存在于编译阶段,编译器完成类型检查后,会把大部分泛型类型参数信息擦除。

例如:

java
class Box<T> {

    private T value;

    public void setValue(
            T value) {

        this.value = value;
    }

    public T getValue() {

        return value;
    }
}

如果:

text
T

没有上界,那么擦除后其主要运行时表示会按照:

java
Object

处理。

可以粗略理解成:

java
class Box {

    private Object value;

    public void setValue(
            Object value) {

        this.value = value;
    }

    public Object getValue() {

        return value;
    }
}

但是需要注意:

类型擦除并不是简单地把 List<Integer> 改写成 List<Object>。

例如:

java
List<Integer> list =
        new ArrayList<>();

Integer value =
        list.get(0);

擦除以后,运行时主要看到的是原始:

text
List

类型,而编译器会在需要的位置生成必要的类型转换。

可以粗略理解为:

java
List list =
        new ArrayList();

Integer value =
        (Integer) list.get(0);

泛型带来的:

text
类型检查

类型推断

强制转换插入

主要由编译器完成。

17. 有上界时如何擦除 ​

如果定义:

java
class Box<
        T extends Number> {

    private T value;

    public T getValue() {

        return value;
    }
}

因为:

text
T

存在上界:

java
Number

所以擦除时通常使用它的上界:

text
Number

而不是:

text
Object

可以粗略理解成:

java
class Box {

    private Number value;

    public Number getValue() {

        return value;
    }
}

所以可以简单记成:

text
<T>
    ↓
擦除后通常是 Object


<T extends Number>
    ↓
擦除后通常是 Number

18. 类型擦除带来的限制 ​

由于泛型存在类型擦除,所以会产生一些比较特殊的限制。

18.1 不能只靠泛型参数进行方法重载 ​

例如:

java
public static void method(
        List<String> list) {
}

和:

java
public static void method(
        List<Date> list) {
}

不能同时存在。

因为擦除以后两者都会变成类似:

java
method(List list)

所以方法签名发生冲突。

18.2 不能直接 new T() ​

例如:

java
class Box<T> {

    public T create() {

        // 编译错误
        // return new T();
    }
}

运行时并不知道:

text
T

究竟是哪一个具体类型,所以无法直接:

java
new T()

如果确实需要创建对象,一般可以传入:

java
Class<T>

或者:

java
Supplier<T>

例如:

java
public static <T> T create(
        Supplier<T> supplier) {

    return supplier.get();
}

调用:

java
User user =
        create(User::new);

18.3 不能创建泛型类型数组 ​

例如:

java
// 编译错误
// T[] array = new T[10];

以及:

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

数组在运行时知道自己的元素类型,而泛型类型参数经过擦除,这两套机制之间存在冲突。

18.4 instanceof 不能检查具体泛型参数 ​

可以:

java
if (obj instanceof List<?>) {
}

但是不能:

java
// 编译错误
// if (obj instanceof
//         List<String>) {
// }

因为运行时:

text
List<String>

List<Integer>

的具体类型参数已经被擦除了。

19. 泛型与基本数据类型 ​

泛型参数不能直接使用基本数据类型。

例如:

java
// 编译错误
// List<int> list;

必须使用包装类型:

java
List<Integer> list =
        new ArrayList<>();

所以:

text
int
    ↓
Integer


long
    ↓
Long


double
    ↓
Double

泛型只能使用:

text
引用类型

作为类型参数。

20. 原始类型 Raw Type ​

Java 为了兼容泛型出现之前的旧代码,仍然允许使用:

java
List list =
        new ArrayList();

这种不写泛型参数的方式叫:

text
Raw Type
原始类型

例如:

java
List list =
        new ArrayList();

list.add("Java");

list.add(100);

这样就重新失去了泛型的类型约束。

取出时:

java
Object value =
        list.get(0);

甚至可能再次遇到:

text
ClassCastException

所以现代 Java 代码中通常应该避免:

java
List

Map

Set

这种原始类型。

推荐:

java
List<String>

Map<String, User>

Set<Long>

21. 泛型在实际开发中的应用 ​

泛型并不仅仅存在于集合框架。

实际 Java 项目中到处都能看到。

21.1 集合 ​

java
List<User>

Set<Long>

Map<String, User>

21.2 统一返回对象 ​

例如:

java
public class Result<T> {

    private int code;

    private String message;

    private T data;
}

使用:

java
Result<User>

Result<List<User>>

Result<Page<User>>

这样一套:

java
Result<T>

就可以承载不同业务数据。

21.3 分页结果 ​

例如:

java
public class PageResult<T> {

    private List<T> records;

    private long total;
}

可以变成:

text
PageResult<User>

PageResult<Order>

PageResult<Product>

21.4 接口抽象 ​

例如:

java
public interface Repository<
        T,
        ID> {

    T findById(ID id);

    void save(T entity);
}

使用:

java
public class UserRepository
        implements Repository<
                User,
                Long> {

    @Override
    public User findById(
            Long id) {

        return null;
    }

    @Override
    public void save(
            User user) {
    }
}

所以泛型真正重要的能力就是:

text
把类型抽象出来
    ↓
复用公共逻辑
    ↓
同时保持类型安全

22. 小结 ​

Java 泛型的核心是:

参数化类型,在编译阶段提供更强的类型约束。

最基础的三种形式:

text
泛型类
    ↓
class Box<T>


泛型接口
    ↓
interface Converter<F, T>


泛型方法
    ↓
<T> T method(T value)

类型约束:

text
<T extends Number>
    ↓
限制类型参数上界

通配符:

text
<?>
    ↓
未知类型


<? extends T>
    ↓
T 或 T 的子类型


<? super T>
    ↓
T 或 T 的父类型

经典原则:

text
PECS

Producer Extends

Consumer Super

类型擦除:

text
泛型主要工作在编译阶段
    ↓
编译器检查类型安全
    ↓
运行时大部分类型参数被擦除

几个容易混淆的重点:

text
Integer extends Number

但是

List<Integer>
并不 extends
List<Number>

以及:

text
<T extends Number>
    ↓
有上界的类型参数


<? extends Number>
    ↓
有上界的通配符

实际开发中,看到:

java
List<T>

Result<T>

Map<K, V>

Class<T>

Supplier<T>

? extends T

? super T

本质都围绕同一个核心:

让代码不知道“具体是什么类型”时,仍然能够在编译阶段保持类型安全,并复用同一套逻辑。

Noctiflorous Garden · MIT License