Java 泛型(Generics)是 Java 5 引入的重要特性之一,它允许在定义类、接口和方法时使用类型参数。泛型的核心思想是将类型由具体的数据类型推迟到使用时再确定,从而提升代码的复用性和类型安全性。
1.泛型的基本概念
1. 什么是泛型?
泛型的本质是参数化类型。参数化类型是指带有类型参数的类或接口在定义类或方法时不指定具体的类型,而是在实例化时传入具体的类型。
- 泛型中不能写基本数据类型
- 指定具体的泛型类型后,传递数据时,可以传递该类类型及其子类类型
- 如果不写泛型,类型默认为Object
/*
List<E> 是 java.util 包下的接口。
E(Element)是类型参数,表示集合中元素的类型。
在代码中你看到的 List<T> 实际上是程序员习惯使用的泛型变量名,与 List<E> 等价。
*/
public interface List<E> extends Collection<E> { ... }
/*
List<E> 是一个泛型接口(类型参数为 E)
List<String> 是一个参数化类型(String 是类型实参)
*/
List<String> list = new ArrayList<>();
list.add("hello");
String str = list.get(0); // 无需强制转换
2.为什么需要泛型
在没有泛型的情况下,集合类默认存储的是 Object
类型,这意味着你可以往集合中添加任何类型的对象,但取出来时需要手动强制转换,容易引发 ClassCastException
。
示例:非泛型带来的问题
List list = new ArrayList();
list.add("hello");
list.add(100); // 编译通过
String str = (String) list.get(1); // 运行时报错:ClassCastException
使用泛型后
泛型确保了编译期的类型检查,避免运行时类型转换错误。
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(100); // 编译错误,不能添加 Integer 类型
String str = list.get(0); // 不需要强制转换
泛型允许我们编写通用的类、接口和方法,而无需为每种数据类型重复实现相同逻辑。
示例:一个通用的容器类
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
你可以这样使用:
一份代码支持多种类型,提高复用性和可维护性。
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
String s = stringBox.getItem(); // 直接获取String类型
Box<Integer> intBox = new Box<>();
intBox.setItem(123);
Integer i = intBox.getItem();
避免强制类型转换(Avoid Casting)
在没有泛型时,从集合中取出元素必须进行强制类型转换,这不仅繁琐,还可能出错。
非泛型写法
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0); // 强制转换
泛型写法
泛型让代码更简洁、清晰,减少出错机会。
List<String> list = new ArrayList<>();
list.add("hello");
String str = list.get(0); // 自动类型匹配
3. 泛型的优点
特性 | 描述 |
---|---|
类型安全 | 避免运行时 ClassCastException |
自动类型转换 | 不需要手动强转 |
代码复用 | 使用泛型编写通用逻辑 |
2.泛型的使用方式
1. 泛型类
通过在类名后加上 <T>
来声明一个泛型类,T
是类型参数(Type Parameter)。可以表示属性类型、方法的返回值类型、参数类型
创建该对象时,该标识确定类型
静态方法不能使用类级别的泛型参数(如class MyClass<T>
中的T
),但可以定义自己的泛型参数。
/*
<>括号中的标识是任意设置的,用来表示类型参数,指代任何数据类型
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思,文章后面部分会讲解示意。
*/
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
// 使用
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
Box<Integer> integerBox = new Box<>();
integerBox.setItem(123);
//多个泛型参数
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; }
}
泛型类的继承
子类也可以保留父类的泛型特性
public class NumberBox<T extends Number> extends Box<T> {
public double getDoubleValue() {
return getValue().doubleValue();
}
}
2. 泛型接口
T
表示实体类型(如 User、Product)。ID
表示主键类型(如 Long、String)。
public interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
void deleteById(ID id);
}
public interface Repository<T, ID> {
T findById(ID id);
void save(T t);
}
public class UserRepository implements Repository<User, Long> {
@Override
public User findById(Long id) {
return null;
}
@Override
public void save(User user) {}
}
实现泛型接口并保留泛型
如果希望实现类也保持泛型特性,可以这样做:
public class GenericRepository<T, ID> implements Repository<T, ID> {
@Override public T findById(ID id) {
// 泛型实现逻辑
return null;
}
@Override public void save(T entity) {
// 泛型保存逻辑
}
@Override public void deleteById(ID id) {
// 泛型删除逻辑
}
}
调用示例:
GenericRepository<User, Long> userRepository = new GenericRepository<>();
User user = userRepository.findById(1L);
userRepository.save(user);
3. 泛型方法
泛型方法的定义需要在返回类型前使用 <T>
来声明一个类型参数,其中 T
是一个占位符,表示任意类型。
方法中参数类型不确定时,泛型方案选择:
1.使用类名后面定义的泛型,所有方法都能用
2.在方法申明上定义自己的泛型,只有本方法能用
/*
<T>:声明一个类型参数。
T value:接收任何类型的参数。
*/
public <T> void printValue(T value) {
System.out.println(value);
}
printValue(10); // 整数
printValue("Hello"); // 字符串
printValue(3.14); // 浮点数
public class Utils {
public static <T> void printArray(T[] array) {
for (T item : array) {
System.out.println(item);
}
}
}
// 调用
Utils.<String>printArray(new String[]{"A", "B"});
Utils.printArray(new Integer[]{1, 2, 3}); // 类型推断
//泛型方法也可以返回泛型类型的数据
public <T> T getValue(T defaultValue) {
return defaultValue;
}
String result = getValue("Default");
Integer number = getValue(100);
4.泛型类与泛型接口的区别
特性 | 泛型类 | 泛型接口 |
---|---|---|
定义方式 | class ClassName<T> |
interface InterfaceName<T> |
主要用途 | 封装通用的数据结构或行为 | 定义通用的行为规范 |
实现方式 | 直接实例化使用 | 需要被类实现后再使用 |
类型约束 | 可以通过 extends 限制类型 |
同样支持 extends 约束 |
多类型参数 | 支持多个泛型参数 | 同样支持多个泛型参数 |
3.泛型通配符
在 Java 泛型中,通配符(Wildcard) 是一种特殊的类型参数,用于表示未知的类型。它增强了泛型的灵活性,特别是在集合类的操作中非常有用。
1.为什么需要泛型通配符?
1. 泛型不具备多态性
在 Java 中,即使 Dog
是 Animal
的子类,List<Dog>
并不是 List<Animal>
的子类型。也就是说,泛型是不变的)。即泛型类型之间不继承其参数类型的多态关系。
List<Dog> dogs = new ArrayList<>();
// List<Animal> animals = dogs; // 编译错误!不能这样赋值
2.为什么泛型不具备多态性?
1. 为了保证类型安全
如果允许 List<Dog>
赋值给 List<Animal>
,就会带来潜在的类型不安全风险。
假设允许这种赋值:
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;
// 如果允许 animals.add(new Cat()); // 合法吗?理论上可以,因为 Cat 是 Animal 子类
Dog dog = dogs.get(0); // 错误 ClassCastException!
这会导致运行时异常,破坏了类型安全性。
因此,Java 在编译期就禁止了这种行为。
3.对比数组的协变性
Java 中的数组是协变的(covariant),即:
Dog[] dogs = new Dog[3]; Animal[] animals = dogs; // 合法
但这其实也存在安全隐患,例如:
animals[0] = new Cat(); // 运行时报错:ArrayStoreException
所以,Java 数组的协变性是在运行时进行类型检查的,而泛型为了避免这种风险,在编译期就禁止了这种操作。
如果你写一个方法用于打印列表中的元素,你可能希望它能接受任何类型的 List
,比如 List<String>
、List<Integer>
等。 那么就需要泛型具有多态性,由此就出现了通配符
4.通配符的类型
1.上界通配符(只能进行只读操作)
在 Java 泛型中,上界通配符 <? extends T>
是一种特殊的泛型表达方式,用于表示某个类型是 T
或其子类型。它提供了一种灵活的方式来处理具有继承关系的泛型集合。
List<? extends T> list;
?
:表示未知类型。extends T
:表示该未知类型是T
或其子类。
List<? extends Number> numbers = new ArrayList<Integer>();
合法赋值包括:
List<Integer>
List<Double>
List<AtomicInteger>
- 等等
Number
的子类列表
1.为什么使用上界通配符?
1. 允许读取为父类型
你可以安全地将集合中的元素当作 T
类型来读取。
public void printNumbers(List<? extends Number> numbers) {
for (Number number : numbers) {
System.out.println(number);
}
}
安全地读取为 Number
,无论实际是 Integer
还是 Double
。
List<Integer> ints = List.of(1, 2);
List<Double> doubles = List.of(3.5, 4.5);
printNumbers(ints); // ✅ 合法
printNumbers(doubles); // ✅ 合法
2. 避免类型不安全的写入
虽然可以读取,但不能向 <? extends T>
集合中添加除 null
外的任何对象。
List<? extends Number> list = new ArrayList<Integer>(); //
list.add(10); // 编译错误!
list.add(null); // 合法(但几乎无意义)
2.为什么上界通配符只能进行只读操作
List<Integer> integers = new ArrayList<>();
List<? extends Number> list = integers;
// list.add(10); // 编译错误!
// list.add(new Integer(5)); // 同样不允许
虽然我们知道 list
实际上是一个 List<Integer>
,并且可以添加 Integer
类型的值,但编译器无法确定 ? extends Number
到底是 Integer
、Double
还是其他子类,所以为了保证类型安全,直接禁止写入操作。
假设允许写入会发生什么?
List<Integer> intList = new ArrayList<>();
List<? extends Number> list = intList;
list.add(3.14); // 如果允许,会怎样?
Integer i = intList.get(0); // ClassCastException!
3.14
是Double
类型。- 虽然它是
Number
的子类,但intList
只能存储Integer
。 - 此时如果允许写入,就会破坏
intList
的类型一致性。
因此,Java 在编译期就阻止了这种风险。
为什么可以添加 null?
list.add(null); // 合法
null
是所有引用类型的合法值。- 它不违反任何类型约束,因为
null
可以被当作任何类型来处理。
2.下界通配符(只写不可读)
在 Java 泛型中,下界通配符 <? super T>
表示一个未知类型,它是 T
或其任意父类。这种通配符用于增强泛型的灵活性,特别是在需要向集合中写入数据时。
List<? super Integer> list;
? super Integer
:表示该列表可以是Integer
、Number
或Object
类型的列表。- 合法赋值包括:
List<Integer> integers = new ArrayList<>(); List<Number> numbers = new ArrayList<>(); List<Object> objects = new ArrayList<>(); List<? super Integer> list1 = integers; // 允许 List<? super Integer> list2 = numbers; // 允许 List<? super Integer> list3 = objects; // 允许
1.为什么下界通配符只能写入,不能读?
你可以安全地向 <? super T>
集合中添加 T
类型或其子类型的对象。
public void addIntegers(List<? super Integer> list) {
list.add(10); // 合法
list.add(new Integer(5)); // 合法
}
原因:
- 编译器知道
? super Integer
是Integer
的父类之一(如Number
或Object
)。 - 所以你传入一个
Integer
,它一定能被接受(因为它是所有可能类型的子类)。
不能读取为具体类型的原因:
虽然你可以写入 Integer
,但你无法确定从集合中读出的元素是什么类型。
List<? super Integer> list = new ArrayList<Number>();
Object obj = list.get(0); // 只能读作 Object // Integer i = list.get(0); // 编译错误
原因:
list
实际上可能是List<Number>
或List<Object>
。- 所以编译器不能保证返回的对象一定是
Integer
,只能保证是Object
类型。
3.无限定通配符
在 Java 泛型中,无限定通配符 <?>
是一种特殊的泛型表达方式,表示“某种未知类型”。它用于定义一个可以接受任何泛型类型的集合或对象。
List<?> list;
?
:表示一个未知的类型。- 可以赋值为任何泛型类型的集合:
List<String> stringList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); List<?> list1 = stringList; // 合法 List<?> list2 = intList; // 合法
1.为什么使用无限定通配符?
1. 适用于只读操作(但只能读作 Object)
你可以遍历集合并读取元素,但只能当作 Object
类型处理:
public void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
调用示例:
printList(stringList); // 输出字符串
printList(intList); // 输出整数
注意:
你不能向 List<?>
中添加任何非 null
元素:
list.add("test"); // 编译错误
list.add(10); // 编译错误
list.add(null); // 合法,但几乎无意义
因为编译器不知道 ?
到底是什么类型,为了保证类型安全,禁止写入。
4.通配符的对比
特性 | 无限定通配符 <?> |
上界通配符 <? extends T> |
下界通配符 <? super T> |
---|---|---|---|
表示类型 | 任意未知类型 | T 或其子类 |
T 或其父类 |
读取能力 | ✅ 只能读作 Object |
✅ 可读为 T |
✅ 可读为 Object |
写入能力 | ❌ 不允许(除 null ) |
❌ 不允许(除 null ) |
✅ 可写入 T 类型 |
使用场景 | 通用只读操作 | 生产者(只读) | 消费者(只写) |
4.PECS 原则详解
PECS(Producer Extends, Consumer Super) 是 Java 泛型中一个非常重要的设计原则,用于指导在使用泛型通配符时如何选择合适的通配符类型,以确保类型安全和代码灵活性。
1.PECS 的含义
角色 | 描述 | 使用的通配符 |
---|---|---|
Producer(生产者) | 只从集合中读取数据 | <? extends T> |
Consumer(消费者) | 只向集合中写入数据 | <? super T> |
✅ 简单记忆:读用 extends,写用 super
2.详细解释
1. Producer Extends
当你只需要从集合中读取数据,并且希望集合可以接受 T
或其子类的任意一种类型时,使用 <? extends T>
。
public void process(List<? extends Number> numbers) {
for (Number number : numbers) {
System.out.println(number.doubleValue());
}
}
调用示例:
List<Integer> ints = List.of(1, 2);
List<Double> doubles = List.of(3.5, 4.5);
process(ints); // 合法
process(doubles); // 合法
- 优点:可以安全地读取为
Number
- 缺点:不能写入任何非
null
元素
2. Consumer Super
当你只需要向集合中写入数据,并希望集合能接受 T
类型及其父类的集合时,使用 <? super T>
。
public void addNumbers(List<? super Integer> list) { list.add(10); list.add(20); }
调用示例:
List<Number> numbers = new ArrayList<>(); addNumbers(numbers); // ✅ 合法
- 优点:可以安全地写入
Integer
或其子类对象 - 缺点:只能读作
Object
,无法还原为具体类型
3.为什么需要 PECS?
Java 泛型不具备多态性(Invariance),即即使 Dog
是 Animal
的子类,List<Dog>
也不是 List<Animal>
的子类。这导致我们在处理集合时面临类型兼容性的挑战。
通过使用通配符并遵循 PECS 原则,我们可以在保持类型安全的前提下,写出更加通用、灵活的代码。
4.典型应用示例
1. 生产者 + 消费者组合使用
public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (T item : src) { dest.add(item); } }
src
是生产者 → 使用<? extends T>
dest
是消费者 → 使用<? super T>
调用示例:
List<Integer> source = List.of(1, 2, 3); List<Number> target = new ArrayList<>(); copy(target, source); // ✅ 合法
5.总结
内容 | 描述 |
---|---|
PECS 原则 | Producer Extends, Consumer Super |
核心思想 | 根据集合是“读”还是“写”来选择合适的通配符 |
优势 | 提高代码复用性、增强类型安全性 |
限制 | 不能同时作为生产者和消费者 |
最佳实践 | 在泛型集合操作中优先考虑通配符,避免直接使用具体泛型类型 |
理解并掌握 PECS 原则,是编写高质量 Java 泛型代码的关键所在。它帮助你在保持类型安全的同时,实现更通用、更灵活的设计。
5.类型擦除(Type Erasure)
Java 的泛型类型擦除(Type Erasure)是 Java 泛型实现的核心机制之一,它指的是在编译期间,泛型类型信息会被移除(擦除),以兼容非泛型的旧代码(即 Java 5 之前的版本)。这意味着泛型只存在于编译阶段,在运行时并不存在具体的泛型类型。
示例:
尽管 List<String>
和 List<Integer>
在源码中指定了不同的泛型类型,但在运行时它们的类型都是 ArrayList
,泛型信息被擦除了。
当把集合定义为string类型的时候,当数据添加在集合当中的时候,仅仅在门口检查了一下数据是否符合String类型, 如果是String类型,就添加成功,当添加成功以后,集合还是会把这些数据当做Object类型处理,当往外获取的时候,集合在把他强转String类型
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true
1.类型擦除的过程
1. 替换所有类型参数为原始类型
- 类型参数如
<T>
被替换为其上界(upper bound)。 - 如果没有指定上界,默认使用
Object
。
例如:
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
编译后相当于:
public class Box {
private Object value; p
ublic void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
2. 插入类型转换代码
编译器会在适当的位置自动插入强制类型转换,确保类型安全。
例如:
Box<String> box = new Box<>();
box.setValue("Hello");
String s = box.getValue(); // 编译器自动插入 (String)box.getValue()
2.类型擦除的影响
影响 | 说明 |
---|---|
无法获取泛型类型信息 | 运行时无法通过反射获取 List<String> 中的 String 类型 |
不能实例化泛型类型 | new T() 是非法的,因为运行时不知道 T 是什么 |
不能创建泛型数组 | T[] array = new T[10]; 是非法的 |
重载方法冲突 | 方法签名在擦除后可能重复,导致编译错误 |
示例:
public void process(List<String> list) {}
public void process(List<Integer> list) {} // 编译错误:方法签名冲突
3.如何绕过类型擦除(获取泛型信息)
虽然 Java 擦除了泛型信息,但在某些情况下可以通过反射获取泛型类型信息,前提是该泛型类型是在声明时明确指定的(不是变量类型)。
示例:获取父类的泛型类型
abstract class Base<T> {
abstract T get();
}
class StringSub extends Base<String> {
@Override String get() { return null; } } // 获取泛型类型
Type type = StringSub.class.getGenericSuperclass();
if (type instanceof ParameterizedType pt) {
Type actualType = pt.getActualTypeArguments()[0];
System.out.println(actualType); // 输出: class java.lang.String
}
5.泛型与继承
- 子类可以继承父类并指定泛型类型。
- 子类也可以继续保留泛型参数。
class Animal {}
class Dog extends Animal {}
class Cage<T> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public T get() {
return animal;
}
}
class DogCage extends Cage<Dog> { // 此处 T 已经固定为 Dog }
6.泛型常见错误
错误示例 | 原因 |
---|---|
List<int> |
泛型不能使用基本类型,应使用 List<Integer> |
new T() |
编译器不知道 T 是什么类型 |
new List<String>[] |
泛型数组不可创建 |
if (obj instanceof List<String>) |
泛型被擦除,无法判断 |
7.Java 明确禁止创建具体泛型参数类型的数组
在 Java 中,泛型数组(如 List<Dog>[]
)与单个泛型对象(如 List<Integer>
)的创建规则完全不同,核心区别在于 数组的协变性(Covariance) 和 泛型的不变性(Invariance)。以下是详细解释:
类型擦除
Java 的泛型是通过编译时的类型检查实现的,但在运行时,泛型信息会被擦除。例如:
List<Dog> list1 = new ArrayList<>();
List<Cat> list2 = new ArrayList<>();
// 运行时 list1 和 list2 都是 List 类型
所以当你尝试创建一个 ArrayList<Dog>[10]
时,运行时实际上只能看到 ArrayList[]
,而无法区分里面存储的是 List<Dog>
还是 List<Cat>
。
数组协变性
Java 数组是协变的(covariant),即:
String[] strings = new String[10]; Object[] objects = strings; // 合法
1. 为什么 List<Dog>[] listArray = new ArrayList<Dog>[10];
不可以?
1.1 核心原因:泛型数组的类型安全问题
Java 的泛型是通过 类型擦除(Type Erasure) 实现的,即泛型信息在运行时被移除。而 数组在运行时保留类型信息,并且支持协变性(Dog[]
是 Animal[]
的子类型),这会导致类型不安全。
示例:泛型数组的潜在风险
// 假设允许创建泛型数组
List<Dog>[] listArray = new ArrayList<Dog>[10];
// 插入错误类型的 List(Dog 是 Animal 的子类)
List<Animal> animalList = new ArrayList<>();
listArray[0] = animalList; // 编译通过,但实际类型不匹配!
// 后续访问时可能出现 ClassCastException
Dog dog = listArray[0].get(0); // 如果 animalList 中有 Cat,此处抛出异常
- 问题:
List<Dog>
和List<Animal>
没有继承关系(泛型是不变的),但数组的协变性允许将List<Animal>
赋值给List<Dog>[]
,导致运行时类型不一致。
1.2 Java 的设计限制
Java 禁止直接创建泛型数组,因为:
- 运行时无法验证数组元素的类型(类型擦除导致)。
- 数组的协变性与泛型的不变性冲突,可能引发类型不安全。
2. 为什么 List<Integer> list01 = new ArrayList<>();
可以?
2.1 钻石操作符(Diamond Operator)
- Java 7 引入:允许在实例化泛型类时省略类型参数,编译器根据上下文自动推断类型。
- 示例:
List<Integer> list01 = new ArrayList<>(); // 合法
- 编译器推断
ArrayList<>
为ArrayList<Integer>
。 - 等价于显式声明:
List<Integer> list01 = new ArrayList<Integer>();
- 编译器推断
2.2 为什么这是安全的?
- 单个对象的类型是固定的:
ArrayList<Integer>
仅存储Integer
类型元素,不会出现多态赋值问题。 - 泛型的不变性不影响单个对象:无需考虑数组的协变性问题。
3. 关键区别总结
复制
特性 | 泛型数组(如 List<Dog>[] ) |
单个泛型对象(如 List<Integer> ) |
---|---|---|
类型检查时机 | 运行时检查(数组保留类型信息) | 编译时检查(泛型通过类型擦除实现) |
协变性 | 支持协变性(Dog[] 是 Animal[] 的子类型) |
不支持协变性(List<Dog> 与 List<Animal> 无继承关系) |
类型安全 | 无法保证(可能插入错误类型) | 完全保证(编译器强制类型匹配) |
Java 允许性 | 不允许(编译警告或错误) | 允许(钻石操作符合法) |
4. 替代方案:如何安全创建泛型数组?
如果需要存储泛型集合的数组,推荐以下方式:
4.1 使用通配符数组
List<?>[] listArray = new ArrayList<?>[10]; // 安全创建
listArray[0] = new ArrayList<Dog>();
listArray[1] = new ArrayList<Animal>(); // 合法,但只能读取(不能添加元素)
- 限制:不能向
List<?>
中添加元素(除了null
)。
4.2 使用嵌套集合
List<List<Dog>> listList = new ArrayList<>();
listList.add(new ArrayList<>()); // 安全
- 优点:完全利用泛型的类型安全性,避免数组的协变性问题。
5. 总结
- 泛型数组不可创建:由于类型擦除和数组的协变性,Java 禁止直接创建泛型数组(如
List<Dog>[]
),以避免运行时类型不安全。 - 单个泛型对象可创建:使用钻石操作符
<>
(Java 7+)可安全创建单个泛型对象(如List<Integer>
),因为类型推断在编译期完成,且不涉及多态赋值。
通过理解数组和泛型的设计差异,可以更安全地编写 Java 代码,避免潜在的类型错误。