【Java】泛型的使用案例
Java 泛型(Generics)是 JDK 5 引入的核心特性,用于在编译阶段提供类型安全检查,避免运行时的ClassCastException,同时实现代码复用。其核心思想是 “参数化类型”—— 将类型定义为参数,在使用时动态指定具体类型。以下详细说明泛型的使用规则、核心概念及实战案例。
【一】泛型的基本定义(类、接口、方法)
泛型可作用于类、接口、方法,分别称为泛型类、泛型接口、泛型方法。
【1】泛型类:类定义时声明类型参数
(1)定义规则:
在类名后添加<类型参数>(如),类型参数可在类的字段、方法参数、返回值中使用。
(2)命名规范:
类型参数通常用单个大写字母表示(约定):
T(Type):表示任意类型
E(Element):表示集合中的元素类型
K(Key):表示键类型
V(Value):表示值类型
(3)案例:泛型类Box(存储任意类型的容器)
// 泛型类定义:T为类型参数
public class Box<T> {
private T content; // 使用泛型参数作为字段类型
// 构造方法:参数类型为T
public Box(T content) {
this.content = content;
}
// 方法返回值为T
public T getContent() {
return content;
}
// 方法参数为T
public void setContent(T content) {
this.content = content;
}
}
// 使用泛型类:指定具体类型(如String、Integer)
public class Test {
public static void main(String[] args) {
// 存储字符串:类型参数为String
Box<String> stringBox = new Box<>("Hello");
String str = stringBox.getContent(); // 无需强制类型转换
// 存储整数:类型参数为Integer
Box<Integer> intBox = new Box<>(123);
Integer num = intBox.getContent();
}
}
(4)效果:
编译时检查类型一致性,若向stringBox存入Integer,会直接编译报错(如stringBox.setContent(123) → 编译错误)。
【2】泛型接口:接口定义时声明类型参数
(1)定义规则:
与泛型类类似,在接口名后添加<类型参数>,实现类需指定具体类型或继续保留泛型。
(2)案例:泛型接口Generator(生成器)
// 泛型接口:定义一个生成T类型对象的方法
public interface Generator<T> {
T generate();
}
// 实现类1:指定具体类型(如String)
public class StringGenerator implements Generator<String> {
@Override
public String generate() {
return "Generated string";
}
}
// 实现类2:保留泛型(泛型实现类)
public class NumberGenerator<T extends Number> implements Generator<T> {
private T seed;
public NumberGenerator(T seed) {
this.seed = seed;
}
@Override
public T generate() {
return seed; // 简化示例,实际可生成复杂数字
}
}
// 使用
public class Test {
public static void main(String[] args) {
Generator<String> strGen = new StringGenerator();
String s = strGen.generate();
Generator<Integer> intGen = new NumberGenerator<>(100);
Integer num = intGen.generate();
}
}
【3】泛型方法:方法定义时声明类型参数
(1)定义规则:
在方法返回值前添加<类型参数>(如),该类型参数仅作用于当前方法,与类的泛型无关(即使类不是泛型类,也可定义泛型方法)。
(2)案例:泛型方法swap(交换数组元素)
public class ArrayUtil {
// 泛型方法:<T>声明类型参数,参数为T[]数组
public static <T> void swap(T[] array, int i, int j) {
if (i < 0 || j < 0 || i >= array.length || j >= array.length) {
return;
}
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
// 使用
public class Test {
public static void main(String[] args) {
// 交换String数组
String[] strs = {"A", "B", "C"};
ArrayUtil.swap(strs, 0, 2); // 交换后:["C", "B", "A"]
// 交换Integer数组
Integer[] nums = {1, 2, 3};
ArrayUtil.swap(nums, 0, 1); // 交换后:[2, 1, 3]
}
}
关键:泛型方法的类型参数由调用时的实际参数类型自动推断(如swap(strs, …)推断T为String)。
【二】泛型的核心使用规则
【1】类型参数必须是引用类型,不能是基本类型
泛型仅支持引用类型(如Integer、String),不支持int、char等基本类型。若需使用基本类型,需用其包装类。
// 错误:基本类型不能作为泛型参数
Box<int> intBox = new Box<>(123); // 编译错误
// 正确:使用包装类
Box<Integer> intBox = new Box<>(123); // 正确
【2】泛型参数在编译时会被 “类型擦除”
Java 泛型是 “编译时特性”,编译后字节码中泛型信息会被擦除,替换为 “原生类型”(Raw Type)。例如:
(1)Box 擦除后为 Box(原生类型)
(2)List 擦除后为 List
类型擦除的影响:
(1)运行时无法获取泛型的具体类型(如instanceof List 编译错误)。
(2)泛型数组不能直接实例化(如new T[10] 编译错误,需通过反射间接创建)。
【3】泛型类 / 接口不能定义静态泛型字段
泛型参数属于实例级别的信息(每个实例的类型参数可能不同),而静态字段属于类级别,无法关联具体的泛型参数。
public class Box<T> {
// 错误:静态字段不能使用泛型参数
private static T staticContent; // 编译错误
}
【4】泛型类的继承规则:泛型不具有协变性
若A是B的子类,Box不是Box的子类(泛型不支持协变),这是为了类型安全。
// Integer是Number的子类,但List<Integer>不是List<Number>的子类
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // 编译错误(禁止协变)
为什么禁止? 若允许,可能导致向numList添加Double(合法,因Double是Number),但intList实际存储的是Integer,运行时会报错。
【5】泛型通配符(?):解决泛型协变问题
通配符用于表示 “未知类型”,分为无界通配符、上界通配符、下界通配符,用于灵活处理泛型的继承关系。
(1)无界通配符<?>:表示任意类型
适用于 “只读取,不写入” 的场景(如打印任意类型的集合)。
// 无界通配符:接收任意类型的List
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
// 使用
List<String> strList = Arrays.asList("A", "B");
List<Integer> intList = Arrays.asList(1, 2);
printList(strList); // 正确
printList(intList); // 正确
限制:无法向List<?>添加任意元素(除null),因类型未知(如list.add(“a”) 编译错误)。
(2)上界通配符<? extends T>:表示 “T 及其子类”
适用于 “只读取” 的场景(如获取集合中元素的最大值,元素必须是Comparable的子类)。
// 上界通配符:元素必须是Number及其子类(Integer、Double等)
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number num : numbers) {
total += num.doubleValue(); // 可安全调用Number的方法
}
return total;
}
// 使用
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);
System.out.println(sum(ints)); // 6.0
System.out.println(sum(doubles)); // 4.0
限制:无法向List<? extends Number>添加元素(除null),因无法确定具体类型(可能是Integer或Double)。
(3)下界通配符<? super T>:表示 “T 及其父类”
适用于 “只写入” 的场景(如向集合添加元素,元素必须是T或其子类)。
// 下界通配符:元素必须是Integer及其父类(Number、Object)
public static void addIntegers(List<? super Integer> list) {
list.add(1); // 正确:Integer是下界类型
list.add(2);
list.add(new Integer(3));
}
// 使用
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();
addIntegers(nums); // 正确:Number是Integer的父类
addIntegers(objs); // 正确:Object是Integer的父类
System.out.println(nums); // [1, 2, 3]
限制:从List<? super Integer>读取元素时,只能用Object接收(因父类类型不确定)。
【三】泛型的高级应用案例
【1】泛型与集合框架(JDK 内置泛型)
Java 集合框架(如ArrayList、HashMap)是泛型的典型应用,通过泛型确保集合中元素类型的一致性。
// ArrayList<String>:只能存储String
List<String> strList = new ArrayList<>();
strList.add("Java");
strList.add(123); // 编译错误(类型不匹配)
// HashMap<String, Integer>:键为String,值为Integer
Map<String, Integer> map = new HashMap<>();
map.put("age", 20);
map.put("score", "90"); // 编译错误(值类型应为Integer)
【2】泛型与反射(创建泛型实例)
由于类型擦除,无法直接new T(),但可通过反射的Class创建泛型实例。
public class ObjectFactory {
// 泛型方法:通过Class对象创建T类型实例
public static <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getConstructor().newInstance(); // 调用无参构造器
}
}
// 使用
public class Test {
public static void main(String[] args) throws Exception {
String str = ObjectFactory.createInstance(String.class);
Integer num = ObjectFactory.createInstance(Integer.class);
}
}
【3】泛型与通配符组合(边界限定)
通过 “上界 + 下界” 组合,实现更灵活的类型控制。例如:复制集合元素(源集合读取,目标集合写入)。
// 从源集合(读取)复制到目标集合(写入)
public static <T> void copy(List<? extends T> source, List<? super T> target) {
for (T item : source) {
target.add(item); // 安全写入:target可接收T类型
}
}
// 使用
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(ints, nums); // 正确:source是Integer(extends Number),target是Number(super Integer)
System.out.println(nums); // [1, 2, 3]
【四】泛型的常见错误与解决方案
【五】总结
Java 泛型的核心价值是编译时类型安全和代码复用,其使用规则可归纳为:
(1)泛型类 / 接口 / 方法通过<类型参数>声明,类型参数需为引用类型。
(2)类型擦除导致运行时无泛型信息,需避免依赖运行时泛型类型的操作。
(3)通配符(<?>、<? extends T>、<? super T>)解决泛型协变问题,分别适用于 “任意类型”“读取场景”“写入场景”。
合理使用泛型可显著减少类型转换代码,降低运行时错误风险,是 Java 开发中不可或缺的特性。