一、什么是泛型?
泛型(Generics) 是一种“参数化类型”的机制。
它允许你在定义类、接口或方法时,使用一个或多个类型参数(如 T、E、K、V 等),在使用时再指定具体类型。
✅ 目的:
编译期类型安全检查
避免强制类型转换
提高代码复用性
二、泛型的基本用法
1. 泛型类(Generic Class)
// 定义一个泛型类 Box<T>
// T 是类型参数,代表任意引用类型(不能是基本类型)
public class Box<T> {
// 成员变量 value 的类型由 T 决定
private T value;
// 设置值,参数类型为 T
public void set(T value) {
this.value = value;
}
// 获取值,返回类型为 T
public T get() {
return value;
}
}使用示例:
public class Main {
public static void main(String[] args) {
// 创建一个只能存放 String 的 Box
Box<String> stringBox = new Box<>();
stringBox.set("Hello, Generics!");
// 无需强制转换,直接获取 String 类型
String content = stringBox.get();
System.out.println(content); // 输出: Hello, Generics!
// 尝试放入 Integer 会编译报错
// stringBox.set(123); // ❌ 编译错误:Integer 不能赋值给 String
}
}2. 泛型接口(Generic Interface)
// 定义一个泛型接口,用于比较两个同类型对象
public interface Comparable<T> {
// compareTo 方法接收一个 T 类型参数,返回 int
int compareTo(T other);
}实现示例:
// 实现 Comparable 接口,指定 T 为 Person
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 实现 compareTo 方法,按年龄比较
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}3. 泛型方法(Generic Method)
注意:泛型方法的类型参数在返回值前声明,与类是否泛型无关。
public class GenericMethodExample {
// <T> 表示这是一个泛型方法,T 是类型参数
// 该方法可以接受任意类型的两个参数并判断是否相等
public static <T> boolean isEqual(T a, T b) {
// 如果两个都为 null,认为相等
if (a == null && b == null) return true;
// 如果其中一个为 null,不相等
if (a == null || b == null) return false;
// 否则调用 equals 比较
return a.equals(b);
}
// 带上界约束的泛型方法:T 必须是 Number 或其子类
public static <T extends Number> double add(T a, T b) {
// 利用 Number 的 doubleValue() 方法统一处理
return a.doubleValue() + b.doubleValue();
}
}调用示例:
public class Main {
public static void main(String[] args) {
// 自动类型推断:编译器知道 T 是 String
boolean same = GenericMethodExample.isEqual("Java", "Java");
System.out.println(same); // true
// 显式指定类型(较少用)
boolean same2 = GenericMethodExample.<Integer>isEqual(10, 20);
System.out.println(same2); // false
// 调用带边界的方法
double sum = GenericMethodExample.add(3.14, 2); // 2 自动装箱为 Integer
System.out.println(sum); // 5.14
}
}4. 多个类型参数
// 定义一个键值对类,支持任意类型的 key 和 value
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 void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
@Override
public String toString() {
return "(" + key + ", " + value + ")";
}
}使用:
Pair<String, Integer> score = new Pair<>("张三", 95);
System.out.println(score); // (张三, 95)三、泛型边界(Bounds)
1. 上界(Upper Bound):<T extends SomeClass>
// T 必须是 Number 的子类(如 Integer, Double 等)
public static <T extends Number> void printDoubleValue(T number) {
// 因为 T 是 Number 的子类,所以可以安全调用 doubleValue()
System.out.println("数值的 double 形式: " + number.doubleValue());
}⚠️ 注意:
extends在这里表示“继承或实现”,可用于类或接口。
2. 多重边界(Multiple Bounds)
// T 必须同时继承 Cloneable 并实现 Comparable<T>
public static <T extends Cloneable & Comparable<T>> void process(T obj) {
// 可以调用 clone() 和 compareTo()
System.out.println("对象可比较且可克隆");
}🔸 多重边界中,类必须写在前面,接口用
&连接。
四、通配符(Wildcards)
通配符 ? 表示“未知类型”,常用于方法参数,增强灵活性。
1. 无界通配符 <?>
// 接受任何类型的 List
public static void printList(List<?> list) {
// 只能读取为 Object(最安全的公共父类)
for (Object item : list) {
System.out.println(item);
}
// 不能添加元素(除了 null)
// list.add("test"); // ❌ 编译错误
}2. 上界通配符 <? extends T> —— 生产者(Producer)
// 接受 List<Number>、List<Integer>、List<Double> 等
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) { // 安全:所有元素都是 Number 或其子类
sum += num.doubleValue();
}
return sum;
}✅ 可读(因为知道是 Number 子类)
❌ 不可写(不知道具体类型,无法保证类型安全)
3. 下界通配符 <? super T> —— 消费者(Consumer)
// 接受 List<Object>、List<Number>、List<Integer> 等(只要能容纳 Integer)
public static void addIntegers(List<? super Integer> list) {
// 可以安全添加 Integer(因为列表至少能存 Integer 或其父类)
list.add(100);
list.add(200);
// 但读取时只能得到 Object
Object obj = list.get(0);
}✅ 可写(因为知道能容纳 T)
❌ 读取类型受限(只能是 Object)
PECS 原则(记住这个口诀!)
五、泛型的限制(重要!)
1. 类型擦除(Type Erasure)
Java 泛型只在编译期存在,运行时会被擦除为原始类型(Raw Type)。
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
// 运行时两者是同一个类!
System.out.println(list1.getClass() == list2.getClass()); // true🔍 后果:无法通过反射直接获取泛型的实际类型(除非通过字段/方法签名保留的
ParameterizedType)。
2. 不能使用基本类型
List<int> numbers; // ❌ 编译错误
List<Integer> numbers; // ✅ 正确,使用包装类3. 不能创建泛型数组
T[] arr = new T[10]; // ❌ 编译错误:无法实例化类型参数
// 替代方案:使用 Object 数组 + 强制转换(需谨慎)
@SuppressWarnings("unchecked")
T[] createArray(int size) {
return (T[]) new Object[size];
}4. 静态成员不能使用类的类型参数
public class Box<T> {
private T value;
// ❌ 错误:静态上下文不能引用非静态类型参数 T
// private static T defaultValue;
// ✅ 正确:静态方法可以有自己的泛型参数
public static <E> E getDefault(E defaultValue) {
return defaultValue;
}
}5. 不能抛出或捕获泛型异常
// ❌ 泛型类不能继承 Throwable
class MyException<T> extends Exception { } // 编译错误
// ❌ 不能 catch 泛型类型
try {
// ...
} catch (SomeGenericException<T> e) { } // 错误六、高级应用示例
1. 利用泛型 + 反射创建实例(需传入 Class)
public class InstanceFactory {
// 通过 Class<T> 在运行时创建 T 的实例
public static <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
}
// 使用
Person p = InstanceFactory.createInstance(Person.class);2. 泛型与函数式接口(Java 8+)
// Function 接口本身就是泛型的
Function<String, Integer> strToLength = String::length;
Integer len = strToLength.apply("Generics"); // 83. 获取泛型实际类型(通过反射)
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
public class TypeReference<T> {
protected final Type type;
protected TypeReference() {
// 获取当前匿名子类的泛型父类信息
Type superClass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
// 使用
TypeReference<List<String>> ref = new TypeReference<List<String>>() {};
System.out.println(ref.getType()); // java.util.List<java.lang.String>💡 这种技巧被 Jackson、Gson 等 JSON 库广泛使用。
七、总结表格
八、最佳实践建议
优先使用泛型,尤其是在集合、工具类中。
避免使用原始类型(Raw Type),如
List而不是List<String>。合理使用通配符提升 API 灵活性。
不要过度设计泛型,保持代码可读性。
理解类型擦除,避免在运行时依赖泛型类型。