泛型
字数: 0 字 时长: 0 分钟
泛型(Generics)是 Java 5 引入的重要机制。
泛型的核心思想可以概括为:
把类型作为参数,在编译阶段对类型进行约束和检查。
例如:
ArrayList<String> list =
new ArrayList<>();
list.add("周芷若");
String name =
list.get(0);这里:
<String>表示这个集合只能按照:
String类型来使用。
泛型主要解决三个问题:
类型安全
↓
编译阶段发现类型错误
减少强制类型转换
↓
取出数据时直接得到目标类型
提高代码复用性
↓
一套代码可以处理多种类型泛型在 Java 中应用非常广泛,例如:
List<E>
Set<E>
Map<K, V>
Optional<T>
Comparable<T>
Future<T>
Callable<V>集合框架只是泛型最典型的应用场景之一,泛型本身应该作为一个独立的 Java 基础知识来理解。
1. 为什么需要泛型
在没有泛型的时候,可以使用:
Object保存任意对象。
例如自己实现一个简单容器:
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];
}
}使用:
Arraylist list =
new Arraylist(10);
list.add("周芷若");
list.add(new Date());由于:
Object可以接收几乎所有对象,所以一个容器中可能混入不同类型的数据。
取出数据时还需要:
String name =
(String) list.get(0);进行强制类型转换。
这会带来两个明显的问题。
1.1 类型不安全
例如:
list.add("周芷若");
list.add(new Date());编译器无法阻止:
String
Date被同时放进容器。
如果错误地转换:
String value =
(String) list.get(1);程序在运行时才会抛出:
ClassCastException1.2 需要大量强制类型转换
因为:
get()返回的是:
Object所以每次取数据都需要:
(String)
(Date)
(User)等强制转换。
泛型就是为了解决这类问题。
使用:
ArrayList<String> list =
new ArrayList<>();以后:
list.add("周芷若");合法。
但是:
// 编译错误
// list.add(new Date());编译器会直接阻止错误类型进入集合。
取出数据:
String value =
list.get(0);也不再需要显式强制转换。
所以:
没有泛型
↓
Object
↓
什么都能放
↓
运行时才可能发现错误
使用泛型
↓
类型参数
↓
编译阶段约束类型
↓
更安全2. 泛型的本质:参数化类型
泛型的本质是:
参数化类型
Parameterized Type普通方法可以把:
数据作为参数:
sum(10, 20);泛型则可以把:
类型参数化。
例如:
Box<String>这里可以理解为:
Box
↓
类型参数 T
↓
实际传入 String定义:
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;
}
}使用:
Box<String> stringBox =
new Box<>("Java");
Box<Integer> integerBox =
new Box<>(100);同一个:
Box<T>既可以用于:
String也可以用于:
Integer这就是泛型带来的代码复用。
3. 泛型类
在类名后面使用:
<T>就可以定义泛型类。
基本语法:
class 类名<T> {
}例如:
public class Box<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(
T value) {
this.value = value;
}
}这里:
T就是:
类型参数
Type Parameter使用时再传入具体类型:
Box<String> box =
new Box<>();
box.setValue("kerwim");
String value =
box.getValue();也可以:
Box<Integer> box =
new Box<>();
box.setValue(18);
Integer value =
box.getValue();3.1 自己实现一个泛型容器
把之前基于:
Object的容器改成泛型:
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];
}
}这里:
E代表:
Element也就是元素类型。
使用字符串:
Arraylist<String> list =
new Arraylist<>(4);
list.add("灭");
list.add("绝");
String value =
list.get(0);使用日期:
Arraylist<Date> list =
new Arraylist<>(4);
list.add(
new Date()
);
Date date =
list.get(0);同一个:
Arraylist<E>可以根据使用者传入的类型参数处理不同数据。
4. 泛型类型参数命名
泛型类型参数理论上可以使用任意合法变量名:
class Box<ABC> {
}语法没有问题。
但 Java 社区通常使用一些约定俗成的单字母名称:
| 类型参数 | 常见含义 |
|---|---|
T | Type,类型 |
E | Element,元素 |
K | Key,键 |
V | Value,值 |
N | Number,数字 |
R | Result / Return,结果 |
S、U | 第二、第三个类型参数 |
例如:
Map<K, V>表示:
K → Key
V → Value多个类型参数可以这样定义:
public class Pair<K, V> {
private K key;
private V value;
}使用:
Pair<String, Integer> pair =
new Pair<>();5. 菱形操作符
早期泛型代码经常写成:
ArrayList<String> list =
new ArrayList<String>();从 Java 7 开始,可以使用:
<>让编译器推断右侧类型:
ArrayList<String> list =
new ArrayList<>();这个:
<>通常称为:
菱形操作符
Diamond Operator现在实际开发一般都使用:
new ArrayList<>();而不会重复写:
new ArrayList<String>();6. 泛型方法
泛型不仅可以定义在类上,也可以单独定义在方法上。
基本语法:
public <T> T method(
T value) {
}需要注意:
<T>写在:
返回值类型之前例如:
public static <T> T echo(
T value) {
return value;
}调用:
String name =
echo("kerwim");
Integer age =
echo(18);编译器可以根据参数推断:
T到底是什么类型。
6.1 泛型类和泛型方法不是一回事
例如:
class Box<E> {
public <T> T convert(
T value) {
return value;
}
}这里存在两个完全独立的类型参数:
E
↓
Box 类的类型参数
T
↓
convert() 方法自己的类型参数例如:
Box<String> box =
new Box<>();
Integer value =
box.convert(100);即使:
Box<E>中的:
E = String也完全不影响:
convert()中的:
T = Integer6.2 泛型方法示例
类似 ArrayList.toArray() 的思路:
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()
);
}
}泛型方法的基本结构可以参考:

调用:
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. 泛型接口
接口同样可以使用泛型。
例如:
public interface Converter<F, T> {
T convert(F source);
}实现:
public class StringToIntegerConverter
implements Converter<
String,
Integer> {
@Override
public Integer convert(
String source) {
return Integer.valueOf(
source
);
}
}调用:
Converter<String, Integer>
converter =
new StringToIntegerConverter();
Integer value =
converter.convert("100");实际项目中的:
Comparator<T>
Comparable<T>
Callable<V>都是典型的泛型接口。
8. 泛型上界 extends
有时候我们并不希望:
<T>可以代表任意类型。
例如:
只允许
Number以及它的子类。
可以使用:
<T extends Number>这种语法称为:
上界
Upper Bound例如:
public class NumberBox<
T extends Number> {
private T value;
public NumberBox(
T value) {
this.value = value;
}
public double doubleValue() {
return value.doubleValue();
}
}允许:
NumberBox<Integer> intBox =
new NumberBox<>(10);
NumberBox<Double> doubleBox =
new NumberBox<>(10.5);但是:
// 编译错误
// NumberBox<String> box =
// new NumberBox<>("Java");因为:
String不是:
Number或者其子类。
8.1 原来的继承例子
假设:
class MieJueShiTai {
@Override
public String toString() {
return "灭绝师太";
}
}class ZhouZhiRuo
extends MieJueShiTai {
@Override
public String toString() {
return "周芷若";
}
}class ZhangWuJi
extends ZhouZhiRuo {
@Override
public String toString() {
return "张无忌";
}
}如果定义:
class Arraylist<
E extends ZhouZhiRuo> {
}那么:
Arraylist<ZhouZhiRuo> list1 =
new Arraylist<>();
Arraylist<ZhangWuJi> list2 =
new Arraylist<>();合法。
但是:
// 编译错误
// Arraylist<MieJueShiTai> list =
// new Arraylist<>();因为:
MieJueShiTai是:
ZhouZhiRuo的父类,而不是它的子类。
所以:
<E extends ZhouZhiRuo>表示:
E 的最大范围是 ZhouZhiRuo
↓
允许 ZhouZhiRuo
↓
以及 ZhouZhiRuo 的子类9. 多重上界
泛型参数还可以同时受到多个条件约束。
例如:
<T extends Number
& Comparable<T>>表示:
T 必须是 Number 的子类
并且
T 必须实现 Comparable<T>例如:
public static <
T extends Number
& Comparable<T>>
T max(
T a,
T b) {
return a.compareTo(b) >= 0
? a
: b;
}需要注意:
如果同时存在类和接口,类必须写在最前面。
例如:
<T extends Number
& Comparable<T>>而不能写成:
// 错误
// <T extends Comparable<T>
// & Number>10. 泛型具有不变性
这一点非常重要。
虽然:
Integer is a Number但是:
List<Integer>并不是:
List<Number>的子类型。
例如:
List<Integer> integers =
new ArrayList<>();
// 编译错误
// List<Number> numbers =
// integers;为什么?
假设这种赋值允许:
List<Integer> integers =
new ArrayList<>();
List<Number> numbers =
integers;那么通过:
numbers就可以:
numbers.add(3.14);但原来的对象实际上是:
List<Integer>这样集合中就混入了:
Double破坏了类型安全。
所以 Java 泛型默认具有:
不变性
Invariance也就是:
A 是 B 的子类并不代表:
Generic<A>是:
Generic<B>的子类。
这也是为什么泛型需要:
通配符
Wildcard11. 泛型通配符 ?
通配符使用:
?表示:
未知类型例如:
List<?>表示:
一个元素具体类型未知的 List。
它可能实际是:
List<String>
List<Integer>
List<User>所以:
public static void printList(
List<?> list) {
for (Object element : list) {
System.out.println(
element
);
}
}可以接受:
List<String> strings =
List.of(
"Java",
"Spring"
);
List<Integer> numbers =
List.of(
1,
2,
3
);
printList(strings);
printList(numbers);11.1 List<?> 能不能添加数据
对于:
List<?> list编译器不知道:
?究竟是什么类型。
所以不能随便:
list.add("Java");
list.add(100);因为:
List<?>可能实际指向:
List<Integer>也可能指向:
List<String>唯一总是安全的值通常是:
null读取时可以安全得到:
Object value =
list.get(0);所以:
List<?>
↓
可以安全读取为 Object
↓
不能随便添加具体对象12. 上限通配符 ? extends T
语法:
<? extends T>表示:
未知类型,但它一定是
T或T的子类型。
例如:
List<? extends Number>它可能指向:
List<Integer>
List<Double>
List<Long>例如:
public static double sum(
List<? extends Number>
numbers) {
double result = 0;
for (Number number :
numbers) {
result +=
number.doubleValue();
}
return result;
}可以传入:
List<Integer> integers =
List.of(
1,
2,
3
);
List<Double> doubles =
List.of(
1.1,
2.2
);
sum(integers);
sum(doubles);12.1 为什么不能随便 add
例如:
List<? extends Number> list;它可能实际引用的是:
List<Integer>也可能是:
List<Double>所以编译器不能确定:
list.add(
Integer.valueOf(1)
);是否安全。
因为如果实际是:
List<Double>就出问题了。
所以:
List<? extends Number>通常:
适合读取
不适合写入读取:
Number number =
list.get(0);是安全的。
13. 下限通配符 ? super T
语法:
<? super T>表示:
未知类型,但一定是
T或T的父类型。
例如:
List<? super Integer>可能实际是:
List<Integer>
List<Number>
List<Object>因此可以安全添加:
Integer对象。
例如:
public static void addNumbers(
List<? super Integer>
list) {
list.add(10);
list.add(20);
}下面都可以:
List<Integer> integers =
new ArrayList<>();
List<Number> numbers =
new ArrayList<>();
List<Object> objects =
new ArrayList<>();
addNumbers(integers);
addNumbers(numbers);
addNumbers(objects);13.1 为什么读取只能得到 Object
对于:
List<? super Integer> list编译器只知道:
实际类型可能是 Integer 的某个父类型但不知道到底是:
Integer
Number
Object所以读取时只能安全认为:
Object value =
list.get(0);不能直接:
// 编译错误
// Integer value =
// list.get(0);因此:
? extends T
↓
适合读
? super T
↓
适合写14. PECS 原则
理解泛型通配符时,有一个非常经典的原则:
PECS全称:
Producer Extends
Consumer Super可以理解为:
如果一个泛型结构主要负责提供数据
↓
使用 ? extends T
如果一个泛型结构主要负责接收数据
↓
使用 ? super T例如复制数据:
public static <T> void copy(
List<? extends T> source,
List<? super T> target) {
for (T element : source) {
target.add(element);
}
}这里:
source
↓
生产数据
↓
? extends T
target
↓
接收数据
↓
? super T可以直接记成:
只读倾向
extends,只写倾向super。
当然这里的:
只读 / 只写是帮助理解的简化说法,并不是指集合真的失去了其他全部操作。
15. extends 在泛型中的两种用途
这里非常容易混淆。
15.1 类型参数上界
例如:
<T extends Number>这里是在:
定义类型参数 T意思是:
T 必须是 Number 或其子类型15.2 通配符上界
例如:
List<? extends Number>这里的:
?代表一个未知类型。
意思是:
某种 Number 或 Number 子类型的 List两者虽然都使用:
extends但意义不同:
<T extends Number>
↓
声明一个有上界的类型参数
<? extends Number>
↓
声明一个有上界的未知类型16. 泛型类型擦除
Java 泛型采用:
类型擦除
Type Erasure实现。
简单理解:
泛型主要存在于编译阶段,编译器完成类型检查后,会把大部分泛型类型参数信息擦除。
例如:
class Box<T> {
private T value;
public void setValue(
T value) {
this.value = value;
}
public T getValue() {
return value;
}
}如果:
T没有上界,那么擦除后其主要运行时表示会按照:
Object处理。
可以粗略理解成:
class Box {
private Object value;
public void setValue(
Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}但是需要注意:
类型擦除并不是简单地把
List<Integer>改写成List<Object>。
例如:
List<Integer> list =
new ArrayList<>();
Integer value =
list.get(0);擦除以后,运行时主要看到的是原始:
List类型,而编译器会在需要的位置生成必要的类型转换。
可以粗略理解为:
List list =
new ArrayList();
Integer value =
(Integer) list.get(0);泛型带来的:
类型检查
类型推断
强制转换插入主要由编译器完成。
17. 有上界时如何擦除
如果定义:
class Box<
T extends Number> {
private T value;
public T getValue() {
return value;
}
}因为:
T存在上界:
Number所以擦除时通常使用它的上界:
Number而不是:
Object可以粗略理解成:
class Box {
private Number value;
public Number getValue() {
return value;
}
}所以可以简单记成:
<T>
↓
擦除后通常是 Object
<T extends Number>
↓
擦除后通常是 Number18. 类型擦除带来的限制
由于泛型存在类型擦除,所以会产生一些比较特殊的限制。
18.1 不能只靠泛型参数进行方法重载
例如:
public static void method(
List<String> list) {
}和:
public static void method(
List<Date> list) {
}不能同时存在。
因为擦除以后两者都会变成类似:
method(List list)所以方法签名发生冲突。
18.2 不能直接 new T()
例如:
class Box<T> {
public T create() {
// 编译错误
// return new T();
}
}运行时并不知道:
T究竟是哪一个具体类型,所以无法直接:
new T()如果确实需要创建对象,一般可以传入:
Class<T>或者:
Supplier<T>例如:
public static <T> T create(
Supplier<T> supplier) {
return supplier.get();
}调用:
User user =
create(User::new);18.3 不能创建泛型类型数组
例如:
// 编译错误
// T[] array = new T[10];以及:
// 编译错误
// List<String>[] lists =
// new List<String>[10];数组在运行时知道自己的元素类型,而泛型类型参数经过擦除,这两套机制之间存在冲突。
18.4 instanceof 不能检查具体泛型参数
可以:
if (obj instanceof List<?>) {
}但是不能:
// 编译错误
// if (obj instanceof
// List<String>) {
// }因为运行时:
List<String>
List<Integer>的具体类型参数已经被擦除了。
19. 泛型与基本数据类型
泛型参数不能直接使用基本数据类型。
例如:
// 编译错误
// List<int> list;必须使用包装类型:
List<Integer> list =
new ArrayList<>();所以:
int
↓
Integer
long
↓
Long
double
↓
Double泛型只能使用:
引用类型作为类型参数。
20. 原始类型 Raw Type
Java 为了兼容泛型出现之前的旧代码,仍然允许使用:
List list =
new ArrayList();这种不写泛型参数的方式叫:
Raw Type
原始类型例如:
List list =
new ArrayList();
list.add("Java");
list.add(100);这样就重新失去了泛型的类型约束。
取出时:
Object value =
list.get(0);甚至可能再次遇到:
ClassCastException所以现代 Java 代码中通常应该避免:
List
Map
Set这种原始类型。
推荐:
List<String>
Map<String, User>
Set<Long>21. 泛型在实际开发中的应用
泛型并不仅仅存在于集合框架。
实际 Java 项目中到处都能看到。
21.1 集合
List<User>
Set<Long>
Map<String, User>21.2 统一返回对象
例如:
public class Result<T> {
private int code;
private String message;
private T data;
}使用:
Result<User>
Result<List<User>>
Result<Page<User>>这样一套:
Result<T>就可以承载不同业务数据。
21.3 分页结果
例如:
public class PageResult<T> {
private List<T> records;
private long total;
}可以变成:
PageResult<User>
PageResult<Order>
PageResult<Product>21.4 接口抽象
例如:
public interface Repository<
T,
ID> {
T findById(ID id);
void save(T entity);
}使用:
public class UserRepository
implements Repository<
User,
Long> {
@Override
public User findById(
Long id) {
return null;
}
@Override
public void save(
User user) {
}
}所以泛型真正重要的能力就是:
把类型抽象出来
↓
复用公共逻辑
↓
同时保持类型安全22. 小结
Java 泛型的核心是:
参数化类型,在编译阶段提供更强的类型约束。
最基础的三种形式:
泛型类
↓
class Box<T>
泛型接口
↓
interface Converter<F, T>
泛型方法
↓
<T> T method(T value)类型约束:
<T extends Number>
↓
限制类型参数上界通配符:
<?>
↓
未知类型
<? extends T>
↓
T 或 T 的子类型
<? super T>
↓
T 或 T 的父类型经典原则:
PECS
Producer Extends
Consumer Super类型擦除:
泛型主要工作在编译阶段
↓
编译器检查类型安全
↓
运行时大部分类型参数被擦除几个容易混淆的重点:
Integer extends Number
但是
List<Integer>
并不 extends
List<Number>以及:
<T extends Number>
↓
有上界的类型参数
<? extends Number>
↓
有上界的通配符实际开发中,看到:
List<T>
Result<T>
Map<K, V>
Class<T>
Supplier<T>
? extends T
? super T本质都围绕同一个核心:
让代码不知道“具体是什么类型”时,仍然能够在编译阶段保持类型安全,并复用同一套逻辑。