泛型类、泛型接口与泛型方法 | JavaSE

泛型类、泛型接口与泛型方法

一、学习目标

完成本章后,你应该能够:

  • 能够独立定义并使用泛型类 Box<T>
  • 能够定义拥有多个类型参数的泛型类,如 Pair<K, V>
  • 能够解释类型参数(Type Parameter)与类型实参(Type Argument)的区别。
  • 能够定义泛型接口,并使用具体类型或继续使用泛型实现接口。
  • 能够定义静态与非静态泛型方法。
  • 能够准确写出泛型方法中 <T> 的位置。
  • 能够区分“类上的 T”和“方法自己的 T”的作用域。
  • 能够理解泛型类型推断的基本机制。
  • 能够根据业务场景判断应该设计泛型类、泛型接口还是泛型方法。

二、核心知识

2.1 从“使用泛型”走向“定义泛型”

上一章我们已经会使用:

ArrayList<String> names = new ArrayList<>();
ArrayList<Integer> scores = new ArrayList<>();

这里:

ArrayList<E>

本身就是一个泛型类。

我们做的只是:

把具体类型传给 E

例如:

ArrayList<String>
          ↑
          E = String

或者:

ArrayList<Integer>
          ↑
          E = Integer

接下来真正需要掌握的是:

如果 Java 可以设计 ArrayList<E>,我们自己能不能设计一个 Box<T>

当然可以。

这就是:

自定义泛型

2.2 什么是泛型类

定义类时声明一个或多个类型参数,这个类就是:

泛型类(Generic Class)

基本格式:

修饰符 class 类名<类型参数> {
}

例如:

public class Box<T> {
}

这里:

T

不是一个已经存在的 Java 类。

它表示:

一个暂时还没有确定、以后由使用者传入的类型。


2.3 第一个泛型类 Box

如果不使用泛型,可以这样设计盒子:

public class Box {
    private Object value;

    public void set(Object value) {
        this.value = value;
    }

    public Object get() {
        return value;
    }
}

问题是:

Box box = new Box();

box.set("Java");

String value = (String) box.get();

取数据时需要:

(String)

强制类型转换。

而且还可能:

box.set(100);

String value = (String) box.get();

最终发生:

ClassCastException

使用泛型后:

public class Box<T> {

    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

现在使用:

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

box.set("Java");

String value = box.get();

整个对象被约束成:

Box<String>

于是:

T = String

那么从源码语义上看:

private T value;

就相当于:

private String value;

set(T) 相当于:

set(String)

而:

T get()

相当于:

String get()

这就是:

把类型作为参数传给类。


2.4 类型参数与类型实参

两个术语必须区分。

定义:

public class Box<T> {
}

其中:

T

叫:

类型参数(Type Parameter)

使用:

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

其中:

String

叫:

类型实参(Type Argument)

可以和普通方法类比:

public static void print(String text) {
}

text 是普通参数。

调用:

print("Java");

"Java" 是实际数据。

泛型则是在:

类型层面

做类似的参数化:

Box<T>
    ↑
类型参数

Box<String>
    ↑
类型实参

2.5 类型参数可以出现在哪里

在:

class Box<T>

定义完成之后,T 可以在类的实例成员中表示一个类型。

例如:

public class Box<T> {

    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }

    public boolean contains(T target) {
        return java.util.Objects.equals(
                value,
                target
        );
    }
}

这里 T 可以用于:

  • 成员变量类型
  • 方法参数类型
  • 返回值类型
  • 局部变量类型
  • 其他泛型类型中的参数

例如:

private ArrayList<T> values;

2.6 多个类型参数

一个泛型类不只能有一个类型参数。

例如:

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<>("Java", 21);

此时:

K = String
V = Integer

所以:

String key = pair.getKey();
Integer value = pair.getValue();

不需要强制类型转换。


2.7 为什么常见 T、E、K、V

Java 泛型并没有规定类型参数必须叫:

T
E
K
V

理论上可以:

class Box<DataType> {
}

但 Java 社区形成了约定:

| 名称 | 常见语义 | | ---- | -------- | | T | Type | | E | Element | | K | Key | | V | Value | | N | Number | | R | Result |

例如:

Map<K, V>

一眼可以理解:

K → Key
V → Value

这种命名提升代码可读性。


三、使用方法

3.1 自定义简化版 MyArrayList

原课程使用 MyArrayList<E> 说明泛型类。

可以设计:

import java.util.ArrayList;

public class MyArrayList<E> {

    private final ArrayList<E> data =
            new ArrayList<>();

    public boolean add(E element) {
        return data.add(element);
    }

    public boolean remove(E element) {
        return data.remove(element);
    }

    public E get(int index) {
        return data.get(index);
    }

    public int size() {
        return data.size();
    }

    @Override
    public String toString() {
        return data.toString();
    }
}

使用:

MyArrayList<String> names =
        new MyArrayList<>();

names.add("Java");
names.add("Spring");

// names.add(100);

String value = names.get(0);

此时整个类内部和外部都保持:

E = String

的类型约束。


3.2 什么是泛型接口

如果在定义接口时声明类型参数:

public interface Data<T> {
}

它就是:

泛型接口(Generic Interface)

例如:

public interface Data<T> {

    void add(T data);

    void delete(T data);

    void update(T data);

    T query(int id);
}

此时接口并没有规定:

T 到底是 Student
还是 Teacher
还是 User

它只定义一套:

针对某种 T 的数据操作协议

3.3 使用具体类型实现泛型接口

假设:

public class Student {
}

可以:

public class StudentData
        implements Data<Student> {

    @Override
    public void add(Student student) {
    }

    @Override
    public void delete(Student student) {
    }

    @Override
    public void update(Student student) {
    }

    @Override
    public Student query(int id) {
        return new Student();
    }
}

这里:

Data<T>
  ↓
Data<Student>

于是接口中的:

void add(T data);

在实现类中自然变成:

void add(Student student);

而:

T query(int id);

变成:

Student query(int id);

3.4 Teacher 使用同一套接口

public class Teacher {
}

再写:

public class TeacherData
        implements Data<Teacher> {

    @Override
    public void add(Teacher teacher) {
    }

    @Override
    public void delete(Teacher teacher) {
    }

    @Override
    public void update(Teacher teacher) {
    }

    @Override
    public Teacher query(int id) {
        return new Teacher();
    }
}

于是:

Data<Student>
Data<Teacher>

共享同一套:

增
删
改
查

接口规范。

这正体现了泛型的:

代码复用
+
类型安全

3.5 泛型接口的另一种实现方式

实现类本身也可以继续保持泛型:

public class DefaultData<T>
        implements Data<T> {

    @Override
    public void add(T data) {
    }

    @Override
    public void delete(T data) {
    }

    @Override
    public void update(T data) {
    }

    @Override
    public T query(int id) {
        return null;
    }
}

使用时再决定:

DefaultData<Student> studentData =
        new DefaultData<>();

DefaultData<Teacher> teacherData =
        new DefaultData<>();

所以实现泛型接口时有两种常见思路:

方案一
实现时确定类型
Data<Student>

方案二
实现类继续泛型化
Data<T>

3.6 什么是泛型方法

如果一个方法自己声明类型参数,它就是:

泛型方法(Generic Method)

基本格式:

修饰符 <类型参数> 返回值类型 方法名(
        参数列表
) {
}

例如:

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

最关键的是:

<T>

必须写在:

返回值类型之前

因此:

public static <T> void print(T value)

是正确的。


3.7 泛型方法 printArray()

原课程中的核心案例是:

打印任意引用类型数组。

如果不用泛型:

public static void printArray(
        String[] array
) {
}

只能打印:

String[]

再写:

public static void printArray(
        Student[] array
) {
}

又只能处理:

Student[]

使用泛型:

public static <T> void printArray(
        T[] array
) {
    for (T value : array) {
        System.out.println(value);
    }
}

现在:

String[] names = {
        "Java",
        "Python"
};

printArray(names);

此时:

T = String

而:

Student[] students = {
        new Student(),
        new Student()
};

printArray(students);

此时:

T = Student

同一个方法完成不同类型数组的处理。


3.8 返回 T

泛型方法也可以返回类型参数。

例如:

public static <T> T first(
        T[] array
) {
    return array[0];
}

调用:

String[] names = {
        "Java",
        "Python"
};

String value = first(names);

编译器能够推断:

T = String

因此:

first(names)

返回值在源码类型系统中就是:

String

3.9 泛型方法交换数组元素

训练题要求实现:

交换任意引用类型数组中的两个位置

可以:

public static <T> void swap(
        T[] array,
        int i,
        int j
) {
    T temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

调用:

String[] names = {
        "Java",
        "Python",
        "C++"
};

swap(names, 0, 2);

也可以:

Integer[] numbers = {
        10,
        20,
        30
};

swap(numbers, 0, 1);

一套算法复用于不同类型。


四、原理与进阶

4.1 泛型类的 T 与泛型方法的 T 不一定是一回事

例如:

public class Box<T> {

    private T value;

    public <U> void print(U data) {
        System.out.println(data);
    }
}

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

Box<T>
    ↑
类级别 T

<U> void print(U)
 ↑
方法级别 U

例如:

Box<String> box = new Box<>();
box.print(100);

此时:

类的 T = String
方法的 U = Integer

二者互不冲突。


4.2 泛型方法有自己的类型参数作用域

例如:

public static <T> T echo(T value) {
    return value;
}

这里 T 的作用域主要就是:

当前方法声明与方法体

它不代表整个类都变成泛型类。

因此:

public class GenericUtils {

    public static <T> T echo(T value) {
        return value;
    }
}

GenericUtils 本身:

不是泛型类

只是拥有一个:

泛型方法

4.3 静态方法为什么不能直接使用类上的 T

例如:

public class Box<T> {

    // 错误思路:
    // public static void print(T value) {
    // }
}

原因是:

T 属于某个具体 Box 实例类型

例如:

Box<String>
Box<Integer>

但:

static

成员属于:

类本身

并不依赖某一个:

Box<String> 实例

因此静态方法如果需要泛型,应该自己声明:

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

这里的:

<T>

属于该静态方法自己。


4.4 类型推断

调用:

public static <T> T echo(T value) {
    return value;
}

可以:

String result = echo("Java");

不用写:

String result =
        GenericUtils.<String>echo("Java");

因为编译器能够根据:

实参类型
上下文

推断:

T = String

这叫:

类型推断(Type Inference)

泛型因此既保持类型安全,又避免代码变得过于繁琐。


4.5 泛型类还是泛型方法?

假设一个:

盒子

从对象创建开始到整个生命周期都处理同一种类型:

Box<String>

那么适合:

泛型类

而:

printArray(...)

只是某一个方法需要适配多种类型,

没有必要让整个工具类都参数化。

因此更适合:

泛型方法

可以使用这个判断:

整个对象都依赖类型
→ 泛型类

只有单个操作依赖类型
→ 泛型方法

五、实践应用

5.1 通用返回结果

以后开发 Java Web 时可能见到类似:

public class Result<T> {

    private int code;
    private String message;
    private T data;
}

那么:

Result<User>

可以保存用户。

Result<List<User>>

可以保存用户集合。

Result<String>

可以保存字符串结果。

这就是泛型类非常典型的工程用途。


5.2 Repository / DAO 接口

例如:

public interface Repository<T> {

    void save(T entity);

    T findById(long id);

    void delete(T entity);
}

可以:

Repository<User>
Repository<Order>
Repository<Product>

用同一套接口表达不同实体的数据操作协议。


5.3 Pair<K,V>

Pair<K,V> 可以表达:

一对相关但类型可能不同的数据

例如:

Pair<String, Integer>

保存:

课程名称 + 分数

或者:

Pair<Long, User>

保存:

用户 ID + 用户对象

六、常见问题

6.1 <T> 到底是什么意思?

表示:

声明一个类型参数 T

它不是:

小于号和大于号比较运算

在泛型语境中:

Box<T>

表示类拥有一个类型参数。


6.2 T 是 Object 吗?

不能简单这样理解。

T 是:

类型变量

最终由类型实参决定。

例如:

Box<String>

则:

T = String

6.3 一个泛型类能有两个泛型吗?

可以。

例如:

Pair<K, V>

甚至可以拥有更多类型参数。

但类型参数数量应该服务于设计,不应为了复杂而复杂。


6.4 泛型接口必须在定义接口时确定类型吗?

不是。

接口定义:

Data<T>

使用者可以:

implements Data<Student>

也可以:

class DataImpl<T>
        implements Data<T>

继续把类型选择留到更后面。


6.5 怎么判断一个方法是不是泛型方法?

关键看它自己有没有声明:

<T>

例如:

public static <T> void print(T data)

是泛型方法。

而:

public void set(T value)

如果 T 来自:

class Box<T>

那么这个方法只是:

泛型类中的普通实例方法

并不是它自己声明了新的泛型参数。


6.6 <T> 为什么写在返回值前?

因为 Java 泛型方法语法规定:

修饰符
→ 类型参数列表
→ 返回值类型
→ 方法名

例如:

public static <T> T first(T[] data)

6.7 泛型方法必须是 static 吗?

不是。

既可以:

public static <T> void print(T value)

也可以:

public <T> void print(T value)

泛型和 static 是两个不同维度。


6.8 泛型类能直接使用 int 吗?

类型实参仍然不能直接使用基本类型:

// Box<int>

应该:

Box<Integer>

然后由自动装箱/拆箱提供基本类型和包装类之间的转换。


七、练习与验收

7.1 知识问答

  1. 什么是泛型类?
  2. 泛型类基本定义格式是什么?
  3. Box<T> 中 T 是类型参数还是类型实参?
  4. Box<String> 中 String 是什么?
  5. E、T、K、V 分别常用于表达什么?
  6. 一个泛型类是否可以有多个类型参数?
  7. 什么是泛型接口?
  8. 泛型接口有哪些常见实现方式?
  9. 什么是泛型方法?
  10. 泛型方法的 <T> 应写在返回值类型的什么位置?
  11. 泛型类中的普通方法与泛型方法有什么区别?
  12. 泛型类的 T 和泛型方法自己的 T/U 作用域有什么区别?
  13. 为什么 static 方法不能直接依赖类级类型参数 T?
  14. 什么是类型推断?
  15. 什么场景更适合泛型类?
  16. 什么场景更适合泛型方法?

7.2 代码阅读

阅读:

public class Box<T> {

    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

使用:

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

box.set("Java");

String value = box.get();

回答:

  1. T 最终代表什么?
  2. set(T) 的参数在当前对象中相当于什么类型?
  3. get() 返回什么类型?
  4. 为什么不需要强制类型转换?

阅读:

public class Demo<T> {

    public <U> U echo(U value) {
        return value;
    }
}

调用:

Demo<String> demo = new Demo<>();
Integer value = demo.echo(100);

回答:

  1. 类级 T 是什么?
  2. 方法级 U 是什么?
  3. 为什么 T = String 不会限制 echo() 只能处理 String?

7.3 手写代码

任务一:Box

从零编写:

Box<T>

要求:

  • 保存一个 T 类型数据。
  • set(T)
  • get()
  • replace(T)
  • 分别创建 Box<String>Box<Integer> 测试。

任务二:Pair<K,V>

编写:

Pair<K,V>

要求:

  • 保存 key。
  • 保存 value。
  • 提供构造器。
  • 提供 getter。
  • 测试 Pair<String,Integer>

任务三:Data

定义:

Data<T>

包含:

add
delete
update
query

然后分别创建:

StudentData
TeacherData

任务四:printArray()

手写:

<T> void printArray(T[] array)

分别测试:

String[]
Integer[]
Student[]

任务五:swap()

编写泛型方法:

swap(T[] array, int i, int j)

交换数组任意两个位置。


7.4 Debug

下面代码存在问题:

public class Box<T> {

    // public static T value;

    // public static void set(T value) {
    // }
}

回答:

  1. 为什么 static 成员不能直接这样使用类级 T?
  2. 如果静态方法真的需要泛型,应该如何修改?

继续 Debug:

public static T echo(T value) {
    return value;
}

回答:

  1. 这段代码缺少什么?
  2. 正确泛型方法声明应该如何书写?

7.5 综合训练

设计一个简单的数据访问体系:

Data<T>
├── StudentData
└── TeacherData

要求:

  1. Data<T> 定义增删改查协议。
  2. StudentData 使用 Data<Student>
  3. TeacherData 使用 Data<Teacher>
  4. 设计一个泛型方法打印查询结果数组。
  5. 不允许使用 Raw Type。
  6. 不允许进行无意义强制类型转换。
  7. 用自己的语言说明泛型如何同时实现:
    • 接口复用
    • 类型安全
    • 减少重复代码

7.6 本章验收

  • [ ] 能闭卷定义 Box<T>
  • [ ] 能闭卷定义 Pair<K,V>
  • [ ] 能解释类型参数与类型实参。
  • [ ] 能闭卷定义泛型接口 Data<T>
  • [ ] 能实现 Data<Student>
  • [ ] 能让实现类继续保持泛型。
  • [ ] 能闭卷写出 <T> void printArray(T[])
  • [ ] 能说明 <T> 为什么在返回值前。
  • [ ] 能解释类级类型参数与方法级类型参数作用域差异。
  • [ ] 能根据场景选择泛型类、接口或方法。