泛型
1. 泛型的作用
Java 泛型(Generics)是 JDK 5 引入的重要特性,它允许在定义类、接口和方法时使用类型参数,从而让代码可以操作多种类型。
1.1 编译时类型安全
没有泛型时,集合类可以存入任意类型的对象,取用时需要强制转换,容易出现运行时异常:
// 没有泛型——不安全
List list = new ArrayList();
list.add("Hello");
list.add(123); // 不小心混入 Integer
String s1 = (String) list.get(0); // 没问题
String s2 = (String) list.get(1); // 运行时抛出 ClassCastException ❌使用泛型后,编译器会在编译期进行类型检查,提前发现问题:
// 有泛型——安全
List<String> list = new ArrayList<>();
list.add("Hello");
list.add(123); // 编译错误 ❌,只能添加 String
String s = list.get(0); // 无需强制转换 ✅1.2 消除强制类型转换
没有泛型时,从集合中取出的元素是 Object 类型,必须手动强转:
// 没有泛型——每次都要强转
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); // 直接赋值,无需强转 ✅2. 泛型类
2.1 基本语法
泛型类是在类名后面加上 <T>,T 是类型参数(Type Parameter),可以是任意标识符,习惯上使用大写字母:
T— Type(类型)E— Element(元素,常用于集合)K— Key(键)V— Value(值)
// 定义一个泛型类 Box,持有任意类型的值
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;
}
public void printType() {
System.out.println("值的类型: " + value.getClass().getName());
}
}使用示例:
public class Main {
public static void main(String[] args) {
// 泛型参数为 String
Box<String> stringBox = new Box<>("Hello Generics");
String value = stringBox.getValue(); // 自动返回 String 类型
System.out.println(value);
// 泛型参数为 Integer
Box<Integer> intBox = new Box<>(100);
Integer intVal = intBox.getValue();
System.out.println(intVal);
// 泛型参数为自定义类型
Box<Person> personBox = new Box<>(new Person("张三"));
personBox.printType(); // 输出: 值的类型: com.example.Person
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}2.2 多个类型参数
一个类可以声明多个类型参数,用逗号分隔:
// 多个类型参数 <K, V>
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;
}
@Override
public String toString() {
return "Pair{" + "key=" + key + ", value=" + value + '}';
}
}使用示例:
public class Main {
public static void main(String[] args) {
// K=String, V=Integer
Pair<String, Integer> pair = new Pair<>("age", 25);
System.out.println(pair); // Pair{key=age, value=25}
// K=Integer, V=String
Pair<Integer, String> student = new Pair<>(1001, "张三");
System.out.println(student); // Pair{key=1001, value=张三}
// 类型推断(菱形语法)
Pair<String, Double> product = new Pair<>("价格", 99.9);
System.out.println(product);
}
}3. 泛型方法
泛型方法是在方法声明中引入类型参数的方法,类型参数放在方法返回类型之前。泛型方法可以存在于普通类中,也可以存在于泛型类中。
public class Util {
// 泛型方法:在返回类型前声明 <T>
public static <T> T getMiddle(T... array) {
if (array == null || array.length == 0) {
return null;
}
return array[array.length / 2];
}
// 两个类型参数的泛型方法
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey())
&& p1.getValue().equals(p2.getValue());
}
// 泛型方法与普通参数混合
public static <T> void printAll(T item, int times) {
for (int i = 0; i < times; i++) {
System.out.print(item + " ");
}
System.out.println();
}
}使用示例:
public class Main {
public static void main(String[] args) {
// 编译器自动推断类型参数
String middle = Util.getMiddle("A", "B", "C", "D", "E");
System.out.println(middle); // C
Integer midNum = Util.getMiddle(10, 20, 30, 40, 50, 60, 70);
System.out.println(midNum); // 40
// 两个类型参数的泛型方法
Pair<String, Integer> p1 = new Pair<>("a", 1);
Pair<String, Integer> p2 = new Pair<>("a", 1);
boolean isEqual = Util.compare(p1, p2);
System.out.println(isEqual); // true
// 泛型方法与普通参数结合
Util.printAll("hello", 3); // hello hello hello
}
}注意: 泛型方法与泛型类中的泛型方法不同。泛型类中的方法如果使用了类的类型参数,那不算泛型方法。只有自己声明了类型参数的方法才是泛型方法。
public class MyClass<T> {
// 这不是泛型方法——它使用的是类的类型参数 T
public T getValue(T t) {
return t;
}
// 这才是泛型方法——它自己声明了类型参数 E
public <E> void printWithType(E e) {
System.out.println(e.getClass().getName());
}
}4. 泛型接口
4.1 基本语法
泛型接口与泛型类的定义方式类似:
// 泛型接口
public interface List<T> {
void add(T element);
T get(int index);
int size();
}4.2 实现类的两种方式
方式一:实现类指定具体类型
// 实现类明确指定类型参数为 String
public class StringArrayList implements List<String> {
private String[] elements = new String[10];
private int count = 0;
@Override
public void add(String element) {
elements[count++] = element;
}
@Override
public String get(int index) {
return elements[index];
}
@Override
public int size() {
return count;
}
}使用:
public class Main {
public static void main(String[] args) {
List<String> list = new StringArrayList();
list.add("Java");
list.add("泛型");
System.out.println(list.get(0)); // Java
}
}方式二:实现类保留泛型参数
// 实现类也保留 T,让调用者决定具体类型
public class GenericArrayList<T> implements List<T> {
private Object[] elements = new Object[10];
private int count = 0;
@Override
public void add(T element) {
elements[count++] = element;
}
@SuppressWarnings("unchecked")
@Override
public T get(int index) {
return (T) elements[index];
}
@Override
public int size() {
return count;
}
}使用:
public class Main {
public static void main(String[] args) {
List<Integer> intList = new GenericArrayList<>(); // 推断为 Integer
List<String> strList = new GenericArrayList<>();
intList.add(100);
strList.add("Hello");
Integer num = intList.get(0); // 自动返回 Integer
String str = strList.get(0); // 自动返回 String
}
}4.3 多个接口限制
实现类可以实现多个泛型接口:
public interface Readable<T> {
T read();
}
public interface Writable<T> {
void write(T t);
}
// 实现多个泛型接口
public class ReadWriteBox<T> implements Readable<T>, Writable<T> {
private T value;
@Override
public T read() {
return value;
}
@Override
public void write(T t) {
this.value = t;
}
}5. 类型擦除
类型擦除(Type Erasure)是 Java 泛型的核心实现机制。Java 泛型是编译期特性,在运行时泛型信息会被擦除。
5.1 什么是类型擦除
在编译时,编译器会对泛型代码进行类型安全检查,然后将泛型信息擦除,替换为原始类型(Raw Type)或边界类型。
// 源代码(带泛型)
public class Box<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}编译后(擦除后)等价于:
// 类型擦除后(替换为 Object)
public class Box {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}如果泛型有上界,则擦除为上界类型:
// 源代码
public class NumericBox<T extends Number> {
private T value;
public double doubleValue() {
return value.doubleValue(); // 可以直接调用 Number 的方法
}
}
// 类型擦除后(替换为 Number)
public class NumericBox {
private Number value;
public double doubleValue() {
return value.doubleValue();
}
}5.2 运行时获取不到泛型类型
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 运行时都是 ArrayList,泛型信息已被擦除
System.out.println(stringList.getClass() == intList.getClass()); // true ✅
System.out.println(stringList instanceof ArrayList); // true ✅
// System.out.println(stringList instanceof ArrayList<String>); // ❌ 编译错误5.3 桥接方法
当子类(或实现类)重写带有泛型的父类方法时,编译器会生成桥接方法(Bridge Method)来维持多态性。
import java.lang.reflect.Method;
public class BridgeMethodDemo {
public static class Parent<T> {
public T getValue() {
return null;
}
}
public static class Child extends Parent<String> {
@Override
public String getValue() {
return "hello";
}
}
public static void main(String[] args) {
Child child = new Child();
// 查看 Child 的所有方法
for (Method m : child.getClass().getDeclaredMethods()) {
System.out.println(m.getName() + " -> " + m.getReturnType().getSimpleName());
}
}
}
// 输出:
// getValue -> String ← 我们自己写的方法
// getValue -> Object ← 编译器生成的桥接方法(桥接方法实际返回 Object,内部调用 String 版本)桥接方法的作用:子类重写时签名是 String getValue(),但父类擦除后是 Object getValue(),如果没有桥接方法,通过父类引用调用时无法正确找到子类方法。
6. 泛型通配符
通配符用 ? 表示,代表未知类型。通配符有三种形式。
6.1 无界通配符 ?
表示类型未知,可以持有任意类型的泛型对象。
public class WildcardDemo {
// 可以接收任意类型的 List
public static void printList(List<?> list) {
for (Object obj : list) { // 只能以 Object 读取
System.out.print(obj + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<String> strings = Arrays.asList("A", "B", "C");
List<Integer> integers = Arrays.asList(1, 2, 3);
printList(strings); // A B C
printList(integers); // 1 2 3
}
}说明:
List<?>可以接收List<String>、List<Integer>等任何类型的 List- 但通过
List<?>只能读取(读为Object类型),不能写入(除 null 外),因为类型未知
List<?> list = new ArrayList<String>();
// list.add("hello"); // ❌ 编译错误,不能写入
// list.add(123); // ❌ 编译错误
list.add(null); // ✅ 只有 null 可以
Object obj = list.get(0); // ✅ 可以读取6.2 上界通配符 ? extends T
表示类型是 T 或 T 的子类型。用于读取场景。
public class ExtendsWildcardDemo {
// 可以接收 Number 及其子类型(Integer、Double 等)的 List
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) {
total += num.doubleValue();
}
return total;
}
public static void main(String[] args) {
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.0);
System.out.println(sum(ints)); // 6.0
System.out.println(sum(doubles)); // 7.0
}
}限制: ? extends T 只能读取,不能写入(除 null 外)。
List<? extends Number> list = new ArrayList<Integer>();
// list.add(3.14); // ❌ 编译错误,不能保证 3.14 是 Integer
// list.add(100); // ❌ 编译错误
Number n = list.get(0); // ✅ 可以以 Number 类型读取原因:编译器只知道它是 Number 的某个子类,但不确定具体是哪个子类,所以不允许写入任何值(null 除外)。
6.3 下界通配符 ? super T
表示类型是 T 或 T 的父类型。用于写入场景。
public class SuperWildcardDemo {
// 可以接收 Integer 及其父类型(Number、Object)的 List
public static void addNumbers(List<? super Integer> list) {
// 可以安全地写入 Integer
list.add(10);
list.add(20);
list.add(30);
}
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
addNumbers(numList);
List<Object> objList = new ArrayList<>();
addNumbers(objList);
// List<String> strList = new ArrayList<>();
// addNumbers(strList); // ❌ 编译错误,String 不是 Integer 的父类
}
}说明:
List<? super Integer>可以写入Integer及其子类- 但读取时只能以
Object类型读取
List<? super Integer> list = new ArrayList<Number>();
list.add(123); // ✅ 可以写入 Integer
list.add(456);
// Integer i = list.get(0); // ❌ 编译错误,不能保证是 Integer
Object obj = list.get(0); // ✅ 只能以 Object 读取7. PECS 原则
PECS 全称 Producer Extends, Consumer Super,由 Joshua Bloch 在《Effective Java》中提出。
7.1 核心思想
- 如果参数化类型是一个生产者(Producer),即你从它里面读取数据,使用
? extends T - 如果参数化类型是一个消费者(Consumer),即你向它里面写入数据,使用
? super T - 如果既是生产者又是消费者,则直接用
T
7.2 PECS 示例
public class PECSDemo {
/**
* 从 src 拷贝元素到 dest
* src 是生产者(读取),用 ? extends T
* dest 是消费者(写入),用 ? super T
*/
public static <T> void copy(List<? extends T> src,
List<? super T> dest) {
for (int i = 0; i < src.size(); i++) {
dest.add(src.get(i));
}
}
public static void main(String[] args) {
List<Integer> src = Arrays.asList(1, 2, 3, 4, 5);
List<Number> dest = new ArrayList<>();
// Integer 是 Number 的子类,可以拷贝
copy(src, dest);
for (Number n : dest) {
System.out.print(n + " ");
}
// 输出: 1 2 3 4 5
}
}7.3 更多 PECS 对比
public class PECSExample {
// ❌ 错误:写入了但无法保证类型安全
// static void fill(List<Object> list, int count) { }
// ✅ 正确:用 ? super T 作为消费者
static <T> void fill(List<? super T> list, T item, int count) {
for (int i = 0; i < count; i++) {
list.add(item);
}
}
// ✅ 正确:用 ? extends T 作为生产者
static <T> T max(List<? extends T> list, Comparator<? super T> comp) {
T max = list.get(0);
for (int i = 1; i < list.size(); i++) {
if (comp.compare(list.get(i), max) > 0) {
max = list.get(i);
}
}
return max;
}
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(10, 50, 30, 80, 20);
// max 方法中: list 是生产者 -> ? extends T
// comparator 是消费者 -> ? super T
Integer maxVal = max(nums, Comparator.naturalOrder());
System.out.println("最大值: " + maxVal); // 80
}
}7.4 口诀记忆
PECS:Producer Extends, Consumer Super
如果你要取数据(生产者),用
? extends T如果你要存数据(消费者),用
? super T
| 场景 | 推荐使用 | 说明 |
|---|---|---|
| 只读 | ? extends T | 可以安全读取为 T 类型 |
| 只写 | ? super T | 可以安全写入 T 类型 |
| 读写 | 直接用 T | 既读又写时不要用通配符 |
8. 泛型限制
由于类型擦除机制,Java 泛型有一些使用上的限制。
8.1 不能 new T()
public class Erased<T> {
private T instance;
public Erased() {
// this.instance = new T(); // ❌ 编译错误,类型擦除后 T 变成 Object,new Object() 没有意义
}
}解决方法: 通过反射创建实例
public class Erased<T> {
private T instance;
public Erased(Class<T> clazz) throws Exception {
this.instance = clazz.getDeclaredConstructor().newInstance();
}
public T getInstance() {
return instance;
}
public static void main(String[] args) throws Exception {
Erased<StringBuilder> e = new Erased<>(StringBuilder.class);
System.out.println(e.getInstance().getClass().getSimpleName()); // StringBuilder
}
}8.2 不能 new T[]
public class ArrayBox<T> {
private T[] array;
public ArrayBox() {
// this.array = new T[10]; // ❌ 编译错误,不能创建泛型数组
}
}解决方法: 创建 Object[] 后转型
@SuppressWarnings("unchecked")
public class ArrayBox<T> {
private T[] array;
public ArrayBox(int size) {
this.array = (T[]) new Object[size]; // ✅ 创建 Object[] 后强转
}
public void set(int index, T value) {
array[index] = value;
}
public T get(int index) {
return array[index];
}
}8.3 不能 instanceof
List<String> stringList = new ArrayList<>();
// if (stringList instanceof List<String>) { // ❌ 编译错误,泛型类型不能用于 instanceof
// }
// 正确的做法:使用原始类型(raw type)或通配符
if (stringList instanceof List) { // ✅ 可以,但会警告
System.out.println("这是一个 List");
}
if (stringList instanceof List<?>) { // ✅ 推荐,无警告
System.out.println("这是一个 List");
}8.4 静态变量不能是泛型
public class StaticGeneric<T> {
// private static T value; // ❌ 编译错误,静态变量不能使用类的类型参数
// 原因:静态变量属于类所有实例共享,但不同的实例可能使用不同的 T
// 例如 StaticGeneric<String> 和 StaticGeneric<Integer> 共享同一个静态变量,
// 无法确定应该是什么类型
}解决方法: 如果确实需要静态泛型,使用泛型方法(在方法级别声明类型参数):
public class StaticGeneric<T> {
// 静态泛型方法——在方法上声明类型参数 ✅
public static <E> E getDefault(E defaultValue) {
return defaultValue;
}
public static void main(String[] args) {
String s = StaticGeneric.getDefault("default");
Integer i = StaticGeneric.getDefault(0);
System.out.println(s); // default
System.out.println(i); // 0
}
}8.5 不能创建泛型异常类
// public class GenericException<T> extends Exception { // ❌ 编译错误,泛型类不能继承 Throwable
// }8.6 不能重载参数类型擦除后相同的方法
public class OverloadDemo {
// public void process(List<String> list) {} // ❌ 编译错误
// public void process(List<Integer> list) {} // ❌ 编译错误
// 擦除后都是 List,方法签名冲突
// 解决方法:使用不同的方法名
public void processString(List<String> list) {}
public void processInteger(List<Integer> list) {}
}9. 小结
本文详细介绍了 Java 泛型的核心知识点,总结如下:
| 知识点 | 关键点 |
|---|---|
| 泛型作用 | 编译时类型安全、消除强制类型转换 |
| 泛型类 | class Box<T>,多个参数 class Pair<K, V> |
| 泛型方法 | 在返回类型前声明类型参数 <T> T method(T t) |
| 泛型接口 | interface List<T>,实现类可指定类型或保留泛型 |
| 类型擦除 | 编译期检查,运行时擦除为原始类型或边界类型;编译器自动生成桥接方法维持多态 |
| 通配符 | ? 无界、? extends T 上界、? super T 下界 |
| PECS 原则 | Producer 用 ? extends(只读),Consumer 用 ? super(只写) |
| 泛型限制 | 不能 new T()、不能 new T[]、不能 instanceof、静态变量不能是泛型等 |
泛型是 Java 语言中最重要的特性之一,掌握泛型有助于编写更安全、更通用、更优雅的 Java 代码。