Java 泛型详解
参数化类型、类型擦除、边界与通配符完整教程
Java 泛型是 JDK 5 引入的参数化类型特性,提供编译期类型安全、消除强制转换和代码复用能力。涵盖泛型类、泛型方法、类型擦除机制、边界与通配符(PECS 原则)等核心概念,并解析了基本类型限制、泛型数组创建、重载冲突等常见问题及解决方案。通过实战示例如泛型栈、缓存和构建器,帮助开发者掌握 Java 泛型的最佳实践与陷阱规避。

参数化类型、类型擦除、边界与通配符完整教程
泛型(Generics)是 JDK 5 引入的特性,实现了参数化类型,使代码可以适用于多种类型。
核心优势:
没有泛型的问题:
// JDK 5 之前:使用 Object 存储
class Box {
private Object obj;
public void set(Object obj) {
this.obj = obj;
}
public Object get() {
return obj;
}
}
// 使用时的问题
Box box = new Box();
box.set("Hello");
String str = (String) box.get(); // 需要强制转换
box.set(123);
String error = (String) box.get(); // 运行时错误!ClassCastException
使用泛型的解决方案:
// JDK 5 之后:使用泛型
class Box<T> {
private T obj;
public void set(T obj) {
this.obj = obj;
}
public T get() {
return obj;
}
}
// 使用时的优势
Box<String> box = new Box<>();
box.set("Hello");
String str = box.get(); // 无需强制转换
// box.set(123); // 编译错误!类型安全
// 单个类型参数
class GenericClass<T> {
private T data;
public GenericClass(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
// 多个类型参数
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;
}
}
| 符号 | 含义 | 示例 |
|---|---|---|
| E | Element(元素) | List |
| K | Key(键) | Map<K, V> |
| V | Value(值) | Map<K, V> |
| T | Type(类型) | Class |
| N | Number(数字) | - |
| ? | 通配符 | List<?> |
// 二元组
public class Tuple2<A, B> {
public final A first;
public final B second;
public Tuple2(A first, B second) {
this.first = first;
this.second = second;
}
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
}
// 三元组
public class Tuple3<A, B, C> extends Tuple2<A, B> {
public final C third;
public Tuple3(A first, B second, C third) {
super(first, second);
this.third = third;
}
@Override
public String toString() {
return "(" + first + ", " + second + ", " + third + ")";
}
}
// 使用示例
public class TupleExample {
public static void main(String[] args) {
// 返回多个值
Tuple2<String, Integer> person = new Tuple2<>("张三", 25);
System.out.println(person); // (张三,25)
// 不同类型组合
Tuple3<String, Integer, Boolean> data = new Tuple3<>("Java", 95, true);
System.out.println(data); // (Java, 95, true)
}
}
泛型方法的类型参数放在返回值之前:
public class GenericMethod {
// 泛型方法
public <T> void print(T item) {
System.out.println(item.getClass().getName() + ": " + item);
}
// 多个类型参数
public <K, V> void printPair(K key, V value) {
System.out.println(key + " = " + value);
}
// 返回泛型类型
public <T> T getFirst(T[] array) {
return array.length > 0 ? array[0] : null;
}
}
public class TypeInference {
public static <T> T identity(T item) {
return item;
}
public static void main(String[] args) {
// 显式指定类型
String s1 = GenericMethod.<String>identity("Hello");
// 类型推断(推荐)
String s2 = identity("Hello");
// 编译器自动推断为 String
Integer i = identity(123); // 自动推断为 Integer
}
}
import java.util.*;
import java.util.function.Function;
public class GenericUtils {
// 交换数组元素
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 查找最大值
public static <T extends Comparable<T>> T max(T[] array) {
if (array == null || array.length == 0) {
return null;
}
T max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i].compareTo(max) > 0) {
max = array[i];
}
}
return max;
}
// 转换 List
public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
List<R> result = new ArrayList<>();
for (T item : list) {
result.add(mapper.apply(item));
}
return result;
}
public static void main(String[] args) {
// 交换示例
String[] names = {"Alice", "Bob", "Charlie"};
swap(names, 0, 2);
System.out.println(Arrays.toString(names)); // [Charlie, Bob, Alice]
// 查找最大值
Integer[] numbers = {3, 7, 2, 9, 1};
System.out.println("最大值:" + max(numbers)); // 9
// 转换 List
List<String> words = Arrays.asList("hello", "world");
List<Integer> lengths = map(words, String::length);
System.out.println(lengths); // [5, 5]
}
}
Java 泛型采用**类型擦除(Type Erasure)**机制实现,这是 Java 泛型与 C++ 模板的本质区别。编译器在编译时检查类型安全,但在生成字节码时会擦除所有泛型类型信息。
擦除规则:
<T>)→ 擦除为 Object<T extends Number>)→ 擦除为第一个边界类型 NumberList<String> 和 List<Integer> → 运行时都是原始类型 Listimport java.util.*;
public class ErasureExample {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
// 运行时类型相同
System.out.println(list1.getClass() == list2.getClass()); // true
System.out.println(list1.getClass()); // class java.util.ArrayList
// 泛型信息在运行时不存在
System.out.println(list1.getClass().getTypeParameters().length); // 1
System.out.println(list1.getClass().getTypeParameters()[0].getName()); // E
}
}
类型擦除带来的限制:
❌ 限制 1:无法创建泛型数组
class Container<T> {
// ❌ 编译错误:Cannot create a generic array of T
// private T[] array = new T[10];
}
❌ 限制 2:无法使用 instanceof 检查泛型类型
class Container<T> {
// ❌ 编译错误:Cannot perform instanceof check against type parameter T
// public boolean check(Object obj) {
// return obj instanceof T;
// }
}
❌ 限制 3:无法创建泛型类型实例
class Container<T> {
// ❌ 编译错误:Cannot instantiate the type T
// public T create() {
// return new T();
// }
}
❌ 限制 4:静态字段不能使用类型参数
class Container<T> {
// ❌ 编译错误:Cannot make a static reference to the non-static type T
// private static T staticItem;
}
✅ 解决方案:传递 Class 对象
通过传递 Class<T> 对象,可以在运行时获取类型信息:
import java.lang.reflect.Array;
class Container<T> {
private Class<T> type;
public Container(Class<T> type) {
this.type = type;
}
// ✅ 使用反射创建数组
@SuppressWarnings("unchecked")
public T[] createArray(int size) {
return (T[]) Array.newInstance(type, size);
}
// ✅ 使用 Class.isInstance() 检查类型
public boolean check(Object obj) {
return type.isInstance(obj);
}
// ✅ 使用反射创建实例
public T create() throws Exception {
return type.getDeclaredConstructor().newInstance();
}
}
// 使用示例
Container<String> container = new Container<>(String.class);
String[] array = container.createArray(5);
System.out.println(container.check("Hello")); // true
System.out.println(container.check(123)); // false
String instance = container.create(); // 创建新的 String 实例
类型参数的边界决定了擦除后的类型:
// 示例 1:无边界 → 擦除为 Object
class Box<T> {
private T item; // 擦除后:private Object item;
public void set(T item) {
this.item = item;
}
// 擦除后:public void set(Object item) { ... }
}
// 示例 2:单个边界 → 擦除为边界类型
class NumberBox<T extends Number> {
private T item; // 擦除后:private Number item;
public void process() {
// ✅ 可以调用 Number 的方法
double value = item.doubleValue();
int intValue = item.intValue();
}
}
// 示例 3:多个边界 → 擦除为第一个边界
interface Readable {
String read();
}
interface Writable {
void write(String data);
}
class DataBox<T extends Number & Readable & Writable> {
private T item; // 擦除后:private Number item
// 注意:第一个边界必须是类(如果有类的话),接口放在后面
public void process() {
// ✅ 可以调用 Number 的方法
item.doubleValue();
// ✅ 也可以调用接口方法(编译器会插入强制转换)
item.read();
item.write("data");
}
}
重要规则:
<T extends Number & Serializable><T extends Serializable & Number>(编译错误)使用 extends 关键字限制类型参数的范围,使其必须是某个类的子类或实现某个接口。
// 上界:T 必须是 Number 或其子类
class NumberProcessor<T extends Number> {
private T number;
public NumberProcessor(T number) {
this.number = number;
}
public double getDoubleValue() {
return number.doubleValue(); // 可以调用 Number 的方法
}
public int compare(T other) {
return Double.compare(number.doubleValue(), other.doubleValue());
}
}
// 使用示例
NumberProcessor<Integer> p1 = new NumberProcessor<>(100);
NumberProcessor<Double> p2 = new NumberProcessor<>(3.14);
System.out.println(p1.getDoubleValue()); // 100.0
System.out.println(p2.compare(2.5)); // 1
// ❌ 编译错误:String 不是 Number 的子类
// NumberProcessor<String> p3 = new NumberProcessor<>("error");
通配符(Wildcard)用于表示未知类型,主要用于方法参数,提供更灵活的类型匹配。
| 通配符 | 含义 | 读操作 | 写操作 | 使用场景 |
|---|---|---|---|---|
<?> | 任意类型 | ✅ 只能读取为 Object | ❌ 不能写入(null 除外) | 只读取,不关心具体类型 |
<? extends T> | T 或 T 的子类 | ✅ 可以读取为 T 类型 | ❌ 不能写入(null 除外) | 从集合中读取数据(生产者) |
<? super T> | T 或 T 的父类 | ❌ 只能读取为 Object | ✅ 可以写入 T 及其子类 | 向集合中写入数据(消费者) |
PECS 原则:Producer Extends, Consumer Super
<? extends T><? super T>import java.util.*;
public class WildcardExample {
// 示例 1:使用 extends 读取数据(生产者)
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) {
total += num.doubleValue(); // ✅ 可以读取为 Number
}
// list.add(1); // ❌ 编译错误:不能写入
return total;
}
// 示例 2:使用 super 写入数据(消费者)
public static void addNumbers(List<? super Integer> list) {
list.add(1); // ✅ 可以写入 Integer
list.add(2);
list.add(3);
// Integer num = list.get(0); // ❌ 编译错误:只能读取为 Object
Object obj = list.get(0); // ✅ 可以读取为 Object
}
// 示例 3:无界通配符
public static void printList(List<?> list) {
for (Object obj : list) {
// ✅ 只能读取为 Object
System.out.println(obj);
}
// list.add("test"); // ❌ 编译错误:不能写入(null 除外)
list.add(null); // ✅ 可以写入 null
}
public static void main(String[] args) {
// extends 示例:可以接受 Number 的任何子类
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("整数求和:" + sum(integers)); // 6.0
System.out.println("浮点数求和:" + sum(doubles)); // 6.6
// super 示例:可以接受 Integer 的任何父类
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // ✅ Number 是 Integer 的父类
addNumbers(objects); // ✅ Object 是 Integer 的父类
System.out.println("numbers: " + numbers); // [1, 2, 3]
System.out.println("objects: " + objects); // [1, 2, 3]
// 无界通配符示例
printList(integers);
printList(Arrays.asList("A", "B", "C"));
}
}
一个类型参数可以有多个边界,用 & 连接。类型必须同时满足所有边界条件。
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
// 多重边界:T 必须同时实现 Flyable 和 Swimmable
class Animal<T extends Flyable & Swimmable> {
private T creature;
public Animal(T creature) {
this.creature = creature;
}
public void move() {
creature.fly(); // ✅ 可以调用 Flyable 的方法
creature.swim(); // ✅ 可以调用 Swimmable 的方法
}
}
// 实现类必须同时实现两个接口
class Duck implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("鸭子飞行");
}
@Override
public void swim() {
System.out.println("鸭子游泳");
}
}
class Fish implements Swimmable {
@Override
public void swim() {
System.out.println("鱼游泳");
}
}
// 使用示例
Animal<Duck> duck = new Animal<>(new Duck());
duck.move(); // 输出:鸭子飞行 \n 鸭子游泳
// ❌ 编译错误:Fish 只实现了 Swimmable,没有实现 Flyable
// Animal<Fish> fish = new Animal<>(new Fish());
多重边界的规则:
<T extends 类 & 接口 1 & 接口 2 & ...>// ✅ 正确:类在前,接口在后
class Example<T extends Number & Comparable<T> & Serializable> {}
// ❌ 错误:接口不能放在类前面
// class Example<T extends Comparable<T> & Number> {}
// ❌ 错误:不能有多个类边界
// class Example<T extends Number & Integer> {}
Java 泛型不支持基本类型(primitive types),必须使用对应的包装类。
// ❌ 错误:不能使用基本类型
// List<int> list = new ArrayList<>();
// Map<double, boolean> map = new HashMap<>();
// ✅ 正确:使用包装类
List<Integer> list = new ArrayList<>();
Map<Double, Boolean> map = new HashMap<>();
// 自动装箱和拆箱
list.add(1); // 自动装箱:int → Integer
int value = list.get(0); // 自动拆箱:Integer → int
// 注意:频繁装箱拆箱会影响性能
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
numbers.add(i); // 每次循环都会装箱,性能开销
}
性能优化建议:
int[] 而不是 List<Integer>由于类型擦除,一个类不能同时实现同一个泛型接口的不同参数化版本。
interface Processor<T> {
void process(T item);
}
// ✅ 正确:实现单一类型
class StringProcessor implements Processor<String> {
@Override
public void process(String item) {
System.out.println("处理字符串:" + item);
}
}
// ❌ 错误:不能同时实现 Processor<String> 和 Processor<Integer>
// 因为类型擦除后,两个接口都变成了 Processor,方法签名冲突
// class MultiProcessor implements Processor<String>, Processor<Integer> {
// @Override
// public void process(String item) { } // 擦除后:process(Object)
// @Override
// public void process(Integer item) { } // 擦除后:process(Object) - 冲突!
// }
// ✅ 解决方案:使用不同的方法名或包装类
class MultiProcessor {
public void processString(String item) {
System.out.println("处理字符串:" + item);
}
public void processInteger(Integer item) {
System.out.println("处理整数:" + item);
}
}
由于类型擦除,Java 不允许直接创建泛型数组。
import java.lang.reflect.Array;
import java.util.*;
// ❌ 错误:不能创建泛型数组
// T[] array = new T[10];
// List<String>[] lists = new ArrayList<String>[10]; // 编译错误
// List<Integer>[] intLists = new List<Integer>[5]; // 编译错误
// ✅ 解决方案 1:使用 ArrayList 代替数组
List<List<String>> lists = new ArrayList<>();
lists.add(new ArrayList<>());
lists.add(new ArrayList<>());
// ✅ 解决方案 2:使用原始类型数组(会有警告)
@SuppressWarnings("unchecked")
List<String>[] lists2 = (List<String>[]) new List[10];
for (int i = 0; i < lists2.length; i++) {
lists2[i] = new ArrayList<>();
}
// ✅ 解决方案 3:使用 Array.newInstance()(需要 Class 对象)
class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
public void set(int index, T value) {
array[index] = value;
}
public T get(int index) {
return array[index];
}
}
// 使用示例
GenericArray<String> stringArray = new GenericArray<>(String.class, 10);
stringArray.set(0, "Hello");
System.out.println(stringArray.get(0)); // Hello
为什么不能创建泛型数组?
由于类型擦除,某些看似合理的方法重载实际上会导致编译错误。
import java.util.*;
// ❌ 错误:擦除后方法签名相同
// class Overload {
// void method(List<String> list) { }
// void method(List<Integer> list) { }
// }
// ✅ 解决方案 1:使用不同的方法名
class Overload {
void processStrings(List<String> list) {
for (String s : list) {
System.out.println("字符串:" + s);
}
}
void processIntegers(List<Integer> list) {
for (Integer i : list) {
System.out.println("整数:" + i);
}
}
}
// ✅ 解决方案 2:使用泛型方法
class GenericOverload {
<T> void process(List<T> list, Class<T> type) {
System.out.println("处理类型:" + type.getSimpleName());
for (T item : list) {
System.out.println(item);
}
}
}
// 使用示例
GenericOverload processor = new GenericOverload();
processor.process(Arrays.asList("A", "B"), String.class);
processor.process(Arrays.asList(1, 2, 3), Integer.class);
使用原始类型(Raw Type)会绕过泛型的类型检查,导致运行时错误。
import java.util.*;
// ❌ 危险:使用原始类型
List rawList = new ArrayList(); // 原始类型,没有类型参数
rawList.add("String");
rawList.add(123);
rawList.add(new Object());
// 运行时错误!
String s = (String) rawList.get(1); // ClassCastException
// ✅ 正确:始终使用泛型
List<String> stringList = new ArrayList<>();
stringList.add("String");
// stringList.add(123); // 编译错误,类型安全
// ⚠️ 警告:原始类型与泛型混用
List<String> genericList = new ArrayList<>();
List rawList2 = genericList; // 警告:unchecked conversion
rawList2.add(123); // 编译通过,但破坏了类型安全
String str = genericList.get(0); // 可能导致 ClassCastException
最佳实践:
List<?> 而不是 List@SuppressWarnings("unchecked") 时要格外小心import java.util.*;
public class GenericStack<T> {
private List<T> items = new ArrayList<>();
public void push(T item) {
items.add(item);
}
public T pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
return items.remove(items.size() - 1);
}
public T peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return items.get(items.size() - 1);
}
public boolean isEmpty() {
return items.isEmpty();
}
public int size() {
return items.size();
}
}
// 使用
GenericStack<String> stack = new GenericStack<>();
stack.push("A");
stack.push("B");
System.out.println(stack.pop()); // B
import java.util.*;
public class Cache<K, V> {
private Map<K, V> cache = new HashMap<>();
private int maxSize;
public Cache(int maxSize) {
this.maxSize = maxSize;
}
public void put(K key, V value) {
if (cache.size() >= maxSize) {
// 简单的 LRU:删除第一个
K firstKey = cache.keySet().iterator().next();
cache.remove(firstKey);
}
cache.put(key, value);
}
public V get(K key) {
return cache.get(key);
}
public boolean containsKey(K key) {
return cache.containsKey(key);
}
}
// 使用
Cache<String, User> userCache = new Cache<>(100);
userCache.put("user1", new User("张三"));
User user = userCache.get("user1");
import java.util.function.BiConsumer;
public class Builder<T> {
private T object;
public Builder(Class<T> clazz) throws Exception {
object = clazz.getDeclaredConstructor().newInstance();
}
public <V> Builder<T> set(BiConsumer<T, V> setter, V value) {
setter.accept(object, value);
return this;
}
public T build() {
return object;
}
}
// 使用
class Person {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
Person person = new Builder<>(Person.class)
.set(Person::setName, "张三")
.set(Person::setAge, 25)
.build();
| 语法 | 说明 | 示例 | 使用场景 |
|---|---|---|---|
<T> | 类型参数 | class Box<T> | 定义泛型类或方法 |
<T extends X> | 上界约束 | <T extends Number> | 限制类型必须是 X 或其子类 |
<T super X> | 下界约束 | <T super Integer> | 限制类型必须是 X 或其父类 |
<?> | 无界通配符 | List<?> | 只读取,不关心具体类型 |
<? extends X> | 上界通配符 | List<? extends Number> | 读取数据(生产者) |
<? super X> | 下界通配符 | List<? super Integer> | 写入数据(消费者) |
<T, U> | 多个类型参数 | Map<K, V> | 需要多个类型参数 |
<T extends A & B> | 多重边界 | <T extends Number & Comparable<T>> | 类型必须满足多个约束 |
| 符号 | 含义 | 常见用法 |
|---|---|---|
E | Element(元素) | List<E>, Set<E> |
K | Key(键) | Map<K, V> |
V | Value(值) | Map<K, V> |
T | Type(类型) | Class<T>, 通用类型 |
N | Number(数字) | 数值类型 |
R | Result(结果) | 返回类型 |
S, U | 第 2、3 个类型 | 多个类型参数时使用 |
PECS 原则(Producer Extends, Consumer Super)
// Producer(生产者):从集合中读取数据 → 使用 extends
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue(); // ✅ 可以读取
}
return total;
}
// Consumer(消费者):向集合中写入数据 → 使用 super
public static void addIntegers(List<? super Integer> list) {
list.add(1); // ✅ 可以写入
list.add(2);
}
使用建议
Class<T> 对象解决类型擦除问题@SuppressWarnings("unchecked") 时要谨慎,并添加注释说明原因常见错误与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 不能创建泛型数组 | 类型擦除 | 使用 ArrayList 或 Array.newInstance() |
| 不能使用基本类型 | 泛型只支持引用类型 | 使用包装类(Integer, Double 等) |
| 不能 instanceof 泛型 | 类型擦除 | 传递 Class<T> 对象 |
| 方法重载冲突 | 擦除后签名相同 | 使用不同方法名或泛型方法 |
| 静态字段不能用类型参数 | 类型参数属于实例 | 使用静态泛型方法 |

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online