泛型类、泛型接口与泛型方法 | 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 知识问答
- 什么是泛型类?
- 泛型类基本定义格式是什么?
Box<T>中 T 是类型参数还是类型实参?Box<String>中 String 是什么?- E、T、K、V 分别常用于表达什么?
- 一个泛型类是否可以有多个类型参数?
- 什么是泛型接口?
- 泛型接口有哪些常见实现方式?
- 什么是泛型方法?
- 泛型方法的
<T>应写在返回值类型的什么位置? - 泛型类中的普通方法与泛型方法有什么区别?
- 泛型类的 T 和泛型方法自己的 T/U 作用域有什么区别?
- 为什么 static 方法不能直接依赖类级类型参数 T?
- 什么是类型推断?
- 什么场景更适合泛型类?
- 什么场景更适合泛型方法?
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();
回答:
T最终代表什么?set(T)的参数在当前对象中相当于什么类型?get()返回什么类型?- 为什么不需要强制类型转换?
阅读:
public class Demo<T> {
public <U> U echo(U value) {
return value;
}
}
调用:
Demo<String> demo = new Demo<>();
Integer value = demo.echo(100);
回答:
- 类级 T 是什么?
- 方法级 U 是什么?
- 为什么
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) {
// }
}
回答:
- 为什么 static 成员不能直接这样使用类级 T?
- 如果静态方法真的需要泛型,应该如何修改?
继续 Debug:
public static T echo(T value) {
return value;
}
回答:
- 这段代码缺少什么?
- 正确泛型方法声明应该如何书写?
7.5 综合训练
设计一个简单的数据访问体系:
Data<T>
├── StudentData
└── TeacherData
要求:
Data<T>定义增删改查协议。StudentData使用Data<Student>。TeacherData使用Data<Teacher>。- 设计一个泛型方法打印查询结果数组。
- 不允许使用 Raw Type。
- 不允许进行无意义强制类型转换。
- 用自己的语言说明泛型如何同时实现:
- 接口复用
- 类型安全
- 减少重复代码
7.6 本章验收
- [ ] 能闭卷定义
Box<T>。 - [ ] 能闭卷定义
Pair<K,V>。 - [ ] 能解释类型参数与类型实参。
- [ ] 能闭卷定义泛型接口
Data<T>。 - [ ] 能实现
Data<Student>。 - [ ] 能让实现类继续保持泛型。
- [ ] 能闭卷写出
<T> void printArray(T[])。 - [ ] 能说明
<T>为什么在返回值前。 - [ ] 能解释类级类型参数与方法级类型参数作用域差异。
- [ ] 能根据场景选择泛型类、接口或方法。