跳到主要内容Java 泛型详解 | 极客日志Javajava
Java 泛型详解
综述由AI生成Java 泛型是 JDK 5 引入的参数化类型特性,提供编译期类型安全、消除强制转换和代码复用能力。涵盖泛型类、泛型方法、类型擦除机制、边界与通配符(PECS 原则)等核心概念,并解析了基本类型限制、泛型数组创建、重载冲突等常见问题及解决方案。通过实战示例如泛型栈、缓存和构建器,帮助开发者掌握 Java 泛型的最佳实践与陷阱规避。
乱七八糟21 浏览 Java 泛型详解
参数化类型、类型擦除、边界与通配符完整教程
一、泛型概述
1.1 什么是泛型
泛型(Generics)是 JDK 5 引入的特性,实现了参数化类型,使代码可以适用于多种类型。
核心优势:
- ✅ 类型安全:编译期检查类型错误
- ✅ 消除强制转换:自动类型转换
- ✅ 代码复用:一套代码适用多种类型
- ✅ 更好的可读性:明确表达意图
1.2 为什么需要泛型
没有泛型的问题:
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();
使用泛型的解决方案:
class Box<T> {
private T obj;
public void set(T obj) {
.obj = obj;
}
T {
obj;
}
}
Box<String> box = <>();
box.set();
box.get();
this
public
get
()
return
new
Box
"Hello"
String
str
=
二、泛型类
2.1 基本语法
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;
}
}
2.2 常用类型参数命名
| 符号 | 含义 | 示例 |
|---|
| E | Element(元素) | List |
| K | Key(键) | Map<K, V> |
| V | Value(值) | Map<K, V> |
| T | Type(类型) | Class |
| N | Number(数字) | - |
| ? | 通配符 | List<?> |
2.3 实战示例:元组类
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);
Tuple3<String, Integer, Boolean> data = new Tuple3<>("Java", 95, true);
System.out.println(data);
}
}
三、泛型方法
3.1 基本语法
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;
}
}
3.2 类型推断
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");
Integer i = identity(123);
}
}
3.3 实战示例:通用工具方法
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;
}
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));
Integer[] numbers = {3, 7, 2, 9, 1};
System.out.println("最大值:" + max(numbers));
List<String> words = Arrays.asList("hello", "world");
List<Integer> lengths = map(words, String::length);
System.out.println(lengths);
}
}
四、类型擦除
4.1 什么是类型擦除
Java 泛型采用**类型擦除(Type Erasure)**机制实现,这是 Java 泛型与 C++ 模板的本质区别。编译器在编译时检查类型安全,但在生成字节码时会擦除所有泛型类型信息。
- 无边界类型参数(
<T>)→ 擦除为 Object
- 有边界类型参数(
<T extends Number>)→ 擦除为第一个边界类型 Number
List<String> 和 List<Integer> → 运行时都是原始类型 List
- 编译器会自动插入类型转换代码,保证类型安全
import 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());
System.out.println(list1.getClass());
System.out.println(list1.getClass().getTypeParameters().length);
System.out.println(list1.getClass().getTypeParameters()[0].getName());
}
}
4.2 擦除的影响
❌ 限制 2:无法使用 instanceof 检查泛型类型
通过传递 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);
}
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"));
System.out.println(container.check(123));
String instance = container.create();
4.3 边界与擦除的关系
class Box<T> {
private T item;
public void set(T item) {
this.item = item;
}
}
class NumberBox<T extends Number> {
private T item;
public void process() {
double value = item.doubleValue();
int intValue = item.intValue();
}
}
interface Readable {
String read();
}
interface Writable {
void write(String data);
}
class DataBox<T extends Number & Readable & Writable> {
private T item;
public void process() {
item.doubleValue();
item.read();
item.write("data");
}
}
- 多个边界时,类必须放在第一位
- 正确:
<T extends Number & Serializable>
- 错误:
<T extends Serializable & Number>(编译错误)
五、边界与通配符
5.1 类型参数边界(extends)
使用 extends 关键字限制类型参数的范围,使其必须是某个类的子类或实现某个接口。
class NumberProcessor<T extends Number> {
private T number;
public NumberProcessor(T number) {
this.number = number;
}
public double getDoubleValue() {
return number.doubleValue();
}
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());
System.out.println(p2.compare(2.5));
5.2 通配符详解
通配符(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 {
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) {
total += num.doubleValue();
}
return total;
}
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
Object obj = list.get(0);
}
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
list.add(null);
}
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("整数求和:" + sum(integers));
System.out.println("浮点数求和:" + sum(doubles));
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers);
addNumbers(objects);
System.out.println("numbers: " + numbers);
System.out.println("objects: " + objects);
printList(integers);
printList(Arrays.asList("A", "B", "C"));
}
}
5.3 多重边界
一个类型参数可以有多个边界,用 & 连接。类型必须同时满足所有边界条件。
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Animal<T extends Flyable & Swimmable> {
private T creature;
public Animal(T creature) {
this.creature = creature;
}
public void move() {
creature.fly();
creature.swim();
}
}
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();
- 最多只能有一个类边界,且必须放在第一位
- 可以有多个接口边界
- 格式:
<T extends 类 & 接口 1 & 接口 2 & ...>
class Example<T extends Number & Comparable<T> & Serializable> {}
六、常见问题与陷阱
6.1 基本类型不能作为类型参数
Java 泛型不支持基本类型(primitive types),必须使用对应的包装类。
List<Integer> list = new ArrayList<>();
Map<Double, Boolean> map = new HashMap<>();
list.add(1);
int value = list.get(0);
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
numbers.add(i);
}
- 对于大量基本类型数据,考虑使用数组
int[] 而不是 List<Integer>
- 或使用第三方库如 Trove、FastUtil 提供的基本类型集合
6.2 不能实现同一泛型接口的不同变体
由于类型擦除,一个类不能同时实现同一个泛型接口的不同参数化版本。
interface Processor<T> {
void process(T item);
}
class StringProcessor implements Processor<String> {
@Override
public void process(String item) {
System.out.println("处理字符串:" + item);
}
}
class MultiProcessor {
public void processString(String item) {
System.out.println("处理字符串:" + item);
}
public void processInteger(Integer item) {
System.out.println("处理整数:" + item);
}
}
6.3 不能创建泛型数组
import java.lang.reflect.Array;
import java.util.*;
List<List<String>> lists = new ArrayList<>();
lists.add(new ArrayList<>());
lists.add(new ArrayList<>());
@SuppressWarnings("unchecked")
List<String>[] lists2 = (List<String>[]) new List[10];
for (int i = 0; i < lists2.length; i++) {
lists2[i] = new ArrayList<>();
}
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));
- 数组在运行时需要知道元素的确切类型(用于类型检查)
- 泛型类型在运行时被擦除,无法提供这个信息
- 如果允许创建泛型数组,会破坏类型安全
6.4 泛型方法重载问题
由于类型擦除,某些看似合理的方法重载实际上会导致编译错误。
import java.util.*;
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);
}
}
}
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);
6.5 原始类型的危险
使用原始类型(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);
List<String> stringList = new ArrayList<>();
stringList.add("String");
List<String> genericList = new ArrayList<>();
List rawList2 = genericList;
rawList2.add(123);
String str = genericList.get(0);
- 永远不要使用原始类型(除非与遗留代码交互)
- 如果不确定类型,使用
List<?> 而不是 List
- 使用
@SuppressWarnings("unchecked") 时要格外小心
七、实战示例
7.1 泛型栈
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());
7.2 泛型缓存
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) {
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");
7.3 泛型构建器
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)
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
- ✅ 优先使用泛型而非原始类型
- ✅ 优先使用泛型方法而非泛型类(更灵活)
- ✅ 使用 PECS 原则选择通配符
- ✅ 避免使用原始类型(Raw Type)
- ✅ 传递
Class<T> 对象解决类型擦除问题
- ✅ 使用
@SuppressWarnings("unchecked") 时要谨慎,并添加注释说明原因
- ❌ 不要忽略编译器的泛型警告
- ❌ 不要在新代码中使用原始类型
| 问题 | 原因 | 解决方案 |
|---|
| 不能创建泛型数组 | 类型擦除 | 使用 ArrayList 或 Array.newInstance() |
| 不能使用基本类型 | 泛型只支持引用类型 | 使用包装类(Integer, Double 等) |
| 不能 instanceof 泛型 | 类型擦除 | 传递 Class<T> 对象 |
| 方法重载冲突 | 擦除后签名相同 | 使用不同方法名或泛型方法 |
| 静态字段不能用类型参数 | 类型参数属于实例 | 使用静态泛型方法 |
参考资源
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online