一、泛型的基本概念
泛型(Generics),也称为"参数化类型",是Java 5引入的重要特性。在引入泛型之前,Java集合框架确实存在明显的类型安全问题。以ArrayList为例,它是一个可以动态增长的数组实现,但早期的ArrayList只能声明为存储Object类型,这意味着:
- 存储阶段:可以存入任何类型的对象(如同时存入String和Integer)
- 取出阶段:必须进行强制类型转换
- 潜在问题:编译时无法发现类型错误,运行时可能抛出ClassCastException
例如:
// 泛型前时代
ArrayList list = new ArrayList();
list.add("Hello");
list.add(123); // 编译通过,但逻辑错误
String str = (String)list.get(1); // 运行时抛出ClassCastException
泛型通过类型参数化完美解决了这个问题。其核心机制包括:
- 类型参数声明:在类/接口定义时使用尖括号声明类型参数,如
class Box<T> - 类型参数使用:在类内部可以将T当作具体类型使用
- 类型实例化:创建对象时指定具体类型,如
Box<String> stringBox = new Box<>();
具体到集合框架中的ArrayList<E>:
E是元素类型参数- 编译器会进行类型检查,确保只能添加指定类型元素
- 取出元素时自动转换,无需显式类型转换
- 提供编译时类型安全,消除运行时类型转换异常
例如改进后的代码:
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译错误,类型不匹配
String str = list.get(0); // 自动转换为String类型
泛型还支持多种高级特性:
- 通配符:
?表示未知类型 - 有界类型参数:
<T extends Number>限制类型范围 - 泛型方法:在方法级别使用类型参数
这些特性共同构成了Java强大的类型系统,使代码更安全、更可读,同时减少了大量的类型转换代码。
二、泛型的定义与使用
(一)泛型类
泛型类是指在定义类时使用类型参数的类,其核心目的是提供类型安全的容器和算法。泛型类的定义格式如下:
class 类名<类型参数列表> {
// 类体中可以使用类型参数
}
类型参数详解
类型参数列表可以包含一个或多个类型参数,每个类型参数通常遵循以下命名约定:
- T(Type):表示任意类型
- E(Element):表示集合中的元素类型
- K(Key):表示映射中的键类型
- V(Value):表示映射中的值类型
- N(Number):表示数值类型
- S, U, V:表示第二、第三、第四类型参数
这些命名约定有助于提高代码可读性,但不影响实际功能。
完整示例与解析
下面是一个更完整的泛型类示例,展示了多种实际应用场景:
/**
* 泛型容器类示例
* @param <T> 容器中存储的元素类型
*/
public class Box<T> {
private T value;
// 构造方法
public Box() {}
public Box(T value) {
this.value = value;
}
// Getter和Setter
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
// 类型安全的比较方法
public boolean isEqual(Box<T> other) {
return this.value.equals(other.value);
}
public static void main(String[] args) {
// 创建存储Integer类型的Box对象
Box<Integer> integerBox = new Box<>(10);
Integer integerValue = integerBox.getValue();
System.out.println("Integer value: " + integerValue);
// 创建存储String类型的Box对象
Box<String> stringBox = new Box<>();
stringBox.setValue("Hello, Generics!");
String stringValue = stringBox.getValue();
System.out.println("String value: " + stringValue);
// 类型安全验证
// integerBox.setValue("string"); // 编译错误,类型不匹配
}
}
多类型参数示例
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// getter和setter方法...
}
// 使用示例
Pair<String, Integer> studentGrade = new Pair<>("Alice", 95);
(二)泛型接口
泛型接口与泛型类类似,在定义接口时使用类型参数,常用于定义通用的操作契约。其定义格式如下:
interface 接口名<类型参数列表> {
// 接口中的方法可以使用类型参数
}
实现方式详解
- 指定具体类型:实现时确定具体类型
- 保持泛型:实现类继续使用类型参数
完整示例
/**
* 泛型生成器接口
* @param <T> 生成的类型
*/
interface Generator<T> {
T generate();
void reset();
boolean hasNext();
}
// 实现时指定具体类型
class RandomNumberGenerator implements Generator<Integer> {
private int count = 0;
private final int maxCount;
public RandomNumberGenerator(int maxCount) {
this.maxCount = maxCount;
}
@Override
public Integer generate() {
count++;
return (int) (Math.random() * 100);
}
@Override
public void reset() {
count = 0;
}
@Override
public boolean hasNext() {
return count < maxCount;
}
}
// 实现时保持泛型
class FixedValueGenerator<T> implements Generator<T> {
private final T fixedValue;
private boolean used = false;
public FixedValueGenerator(T fixedValue) {
this.fixedValue = fixedValue;
}
@Override
public T generate() {
if (used) {
throw new IllegalStateException("Generator already used");
}
used = true;
return fixedValue;
}
@Override
public void reset() {
used = false;
}
@Override
public boolean hasNext() {
return !used;
}
}
public class GenericInterfaceDemo {
public static void main(String[] args) {
// 使用具体类型实现
Generator<Integer> numberGenerator = new RandomNumberGenerator(3);
while (numberGenerator.hasNext()) {
System.out.println("Generated number: " + numberGenerator.generate());
}
// 使用泛型实现
Generator<String> stringGenerator = new FixedValueGenerator<>("Hello");
System.out.println("Generated string: " + stringGenerator.generate());
try {
stringGenerator.generate(); // 抛出异常
} catch (Exception e) {
System.out.println("Expected error: " + e.getMessage());
}
stringGenerator.reset();
System.out.println("After reset: " + stringGenerator.generate());
}
}
(三)泛型方法
泛型方法是指在方法声明中使用类型参数的方法,它可以在普通类或泛型类中定义。其定义格式如下:
修饰符 <类型参数列表> 返回值类型 方法名(参数列表) {
// 方法体
}
特点与优势
- 类型参数的作用域仅限于方法内部
- 可以用于静态方法
- 方法调用时可以显式或隐式指定类型参数
完整示例
import java.util.Arrays;
import java.util.List;
public class GenericMethodDemo {
// 基本泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
// 返回泛型类型的方法
public static <T> T getMiddle(T[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("Array is empty or null");
}
return array[array.length / 2];
}
// 受限泛型方法 - 限定类型参数
public static <T extends Comparable<T>> T max(T a, T b, T c) {
T max = a;
if (b.compareTo(max) > 0) {
max = b;
}
if (c.compareTo(max) > 0) {
max = c;
}
return max;
}
// 泛型方法与可变参数
@SafeVarargs
public static <T> List<T> makeList(T... elements) {
return Arrays.asList(elements);
}
public static void main(String[] args) {
// 基本使用
Integer[] intArray = {1, 2, 3, 4, 5};
String[] stringArray = {"Hello", "World", "Generics"};
System.out.print("Integer array: ");
printArray(intArray);
System.out.print("String array: ");
printArray(stringArray);
// 获取中间元素
System.out.println("Middle of int array: " + getMiddle(intArray));
System.out.println("Middle of string array: " + getMiddle(stringArray));
// 受限泛型方法
System.out.println("Max of 3, 7, 5: " + max(3, 7, 5));
System.out.println("Max of 'apple', 'orange', 'banana': " +
max("apple", "orange", "banana"));
// 泛型方法与集合
List<Integer> intList = makeList(1, 2, 3);
List<String> stringList = makeList("A", "B", "C");
System.out.println("Integer list: " + intList);
System.out.println("String list: " + stringList);
}
}
静态泛型方法注意事项
静态泛型方法特别有用,因为它们不依赖于类的类型参数。例如:
public class Algorithm {
// 静态泛型方法
public static <T> T max(T x, T y) {
return ((Comparable<T>)x).compareTo(y) > 0 ? x : y;
}
}
泛型方法与类型推断
Java编译器通常可以推断泛型方法的类型参数,使代码更简洁:
// 不需要指定类型参数
GenericMethodDemo.printArray(new String[]{"a", "b", "c"});
// 也可以显式指定
GenericMethodDemo.<String>printArray(new String[]{"a", "b", "c"});
三、泛型的基础操作指令
(一)类型通配符
Java 泛型中的类型通配符是处理泛型类型灵活性的重要工具,主要用于处理泛型类、接口和方法中未知类型的情况。类型通配符可以显著提高代码的复用性和灵活性。
1. 无界通配符
无界通配符?表示可以匹配任何类型,相当于? extends Object。它特别适用于以下场景:
- 当方法实现不需要依赖具体类型参数时
- 只需要使用Object类中的方法时
- 需要处理不同类型的泛型对象时
应用示例:
public class UnboundedWildcardDemo {
// 定义一个泛型Box类
public static class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// 使用无界通配符的方法
public static void printBox(Box<?> box) {
System.out.println("Box value: " + box.getValue());
// 可以调用Object的方法
System.out.println("Hash code: " + box.getValue().hashCode());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.setValue(100);
printBox(integerBox); // 输出: Box value: 100
Box<String> stringBox = new Box<>();
stringBox.setValue("Test");
printBox(stringBox); // 输出: Box value: Test
Box<Object> objectBox = new Box<>();
objectBox.setValue(new Object());
printBox(objectBox); // 输出: Box value: java.lang.Object@...
}
}
2. 上界通配符
上界通配符? extends Type表示可以匹配指定类型及其所有子类型,适用于需要读取元素但不需要修改的场景。
特点:
- 允许读取元素(返回类型为上界类型)
- 不允许添加元素(除null外)
- 常用于产生数据的场景
应用示例:
public class UpperBoundedWildcardDemo {
public static class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// 计算各种Number类型的总和
public static double sumOfNumbers(Box<? extends Number> box) {
Number number = box.getValue();
return number.doubleValue();
}
// 处理Number列表
public static void processNumbers(List<? extends Number> numbers) {
for (Number num : numbers) {
System.out.println(num.doubleValue());
}
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.setValue(10);
System.out.println("Sum: " + sumOfNumbers(integerBox)); // 输出: 10.0
Box<Double> doubleBox = new Box<>();
doubleBox.setValue(20.5);
System.out.println("Sum: " + sumOfNumbers(doubleBox)); // 输出: 20.5
List<Integer> intList = Arrays.asList(1, 2, 3);
processNumbers(intList); // 输出三个数字
// 下面的代码会编译错误,因为String不是Number的子类
// Box<String> stringBox = new Box<>();
// stringBox.setValue("10");
// System.out.println("Sum: " + sumOfNumbers(stringBox));
}
}
3. 下界通配符
下界通配符? super Type表示可以匹配指定类型及其所有父类型,适用于需要写入元素但不需要精确读取的场景。
特点:
- 允许添加元素(只要类型是下界类型的子类)
- 读取时只能获取Object类型
- 常用于消费数据的场景
应用示例:
public class LowerBoundedWildcardDemo {
public static class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// 只能向Integer或其父类的Box中添加Integer
public static void addInteger(Box<? super Integer> box) {
box.setValue(100); // 可以安全添加Integer
// 读取时只能获取Object
Object val = box.getValue();
System.out.println("Added value: " + val);
}
// 填充数字到列表
public static void fillNumbers(List<? super Number> list) {
list.add(1); // Integer
list.add(2.0); // Double
list.add(3L); // Long
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
addInteger(integerBox);
System.out.println("Integer box value: " + integerBox.getValue()); // 输出: 100
Box<Number> numberBox = new Box<>();
addInteger(numberBox);
System.out.println("Number box value: " + numberBox.getValue()); // 输出: 100
Box<Object> objectBox = new Box<>();
addInteger(objectBox);
System.out.println("Object box value: " + objectBox.getValue()); // 输出: 100
List<Object> objList = new ArrayList<>();
fillNumbers(objList);
System.out.println("Filled list: " + objList); // 输出: [1, 2.0, 3]
// 下面的代码会编译错误,因为String不是Integer的父类
// Box<String> stringBox = new Box<>();
// addInteger(stringBox);
}
}
(二)泛型擦除
1. 泛型擦除机制
Java的泛型是通过类型擦除(Type Erasure)实现的,这是为了保持与Java 5之前版本的二进制兼容性。擦除过程包括:
-
类型参数替换:
- 无界类型参数(
<T>)替换为Object - 有上界的类型参数(
<T extends Number>)替换为上界类型(Number)
- 无界类型参数(
-
桥接方法生成:
- 编译器会生成桥接方法来保持多态性
-
类型检查:
- 编译时进行类型安全检查
- 运行时类型信息被擦除
2. 擦除示例
public class GenericErasureDemo {
public static class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// 泛型方法擦除示例
public static <T> void genericMethod(T param) {
System.out.println(param.getClass());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
Box<String> stringBox = new Box<>();
// 运行时类型相同
System.out.println(integerBox.getClass() == stringBox.getClass()); // true
// 泛型方法擦除
genericMethod(100); // 输出: class java.lang.Integer
genericMethod("hello"); // 输出: class java.lang.String
// 类型转换示例
Box rawBox = integerBox; // 合法,但会丢失类型信息
rawBox.setValue("string"); // 编译通过,运行时会抛ClassCastException
try {
Integer value = integerBox.getValue(); // 隐式类型转换
} catch (ClassCastException e) {
System.out.println("类型转换异常");
}
}
}
3. 擦除带来的限制
-
不能创建泛型数组:
// 编译错误 // List<String>[] array = new List<String>[10]; -
instanceof检查无效:
// 编译错误 // if (obj instanceof List<String>) {...} -
不能抛出或捕获泛型异常:
// 编译错误 // class MyException<T> extends Exception {...} -
不能重载类型擦除后签名相同的方法:
// 编译错误 // void method(List<String> list) {...} // void method(List<Integer> list) {...}
4. 绕过擦除的限制
-
使用Class对象保留类型信息:
public class TypeToken<T> { private final Class<T> type; public TypeToken(Class<T> type) { this.type = type; } public Class<T> getType() { return type; } } -
使用反射获取泛型信息:
public abstract class GenericType<T> { private final Type type; protected GenericType() { Type superclass = getClass().getGenericSuperclass(); this.type = ((ParameterizedType)superclass).getActualTypeArguments()[0]; } public Type getType() { return type; } } -
使用注解处理器:
- 在编译时处理泛型信息
- 生成类型安全的代码
泛型擦除是Java泛型实现的核心机制,理解它对于编写正确的泛型代码至关重要,也能帮助开发者规避一些常见的陷阱。
四、泛型的注意事项
(一)不能使用基本数据类型作为类型参数
在 Java 中,泛型的类型参数必须是引用类型(reference type),不能是基本数据类型(primitive type)。这是 Java 泛型系统的一个重要限制。
具体表现:
- 编译错误示例:
Box<int> box = new Box<>();会导致编译错误 - 正确用法:必须使用对应的包装类
Box<Integer> box = new Box<>();
深层原因:
- Java 的泛型是通过类型擦除(type erasure)实现的
- 在编译后,类型参数会被替换为 Object 或其边界类型
- 基本数据类型(如 int、double 等)不是 Object 的子类
- 自动装箱/拆箱机制只适用于具体场景,不能应用于泛型的类型参数
实际影响:
- 使用泛型容器存储数值时会有额外的装箱开销
- 例如
List<Integer>实际上存储的是 Integer 对象而非原始 int 值
(二)不能创建泛型数组
Java 不允许直接创建泛型数组,这是出于类型安全的考虑。
常见错误:
T[] array = new T[10]; // 编译错误
解决方案:
- 使用反射 API 创建数组:
public class GenericArrayDemo {
public static <T> T[] createArray(Class<T> clazz, int size) {
return (T[]) Array.newInstance(clazz, size);
}
public static void main(String[] args) {
Integer[] intArray = createArray(Integer.class, 5);
intArray[0] = 1;
System.out.println(intArray[0]);
}
}
- 使用 ArrayList 等集合类替代数组
原因分析:
- 数组在运行时需要知道其确切的元素类型
- 由于类型擦除,泛型类型信息在运行时不可用
- 直接创建泛型数组可能导致类型安全问题
应用场景:
- 当需要实现泛型数据结构(如栈、队列)时
- 在性能敏感的场合需要数组而非集合时
(三)不能在静态成员中使用类型参数
泛型类中的静态成员不能使用类的类型参数,这是由 Java 的类加载机制决定的。
错误示例:
public class StaticGenericDemo<T> {
// 以下两行都会导致编译错误
private static T value;
public static T getValue() {
return value;
}
}
正确理解:
- 静态成员属于类,而非类的实例
- 在类加载时,具体的类型参数尚未确定
- 所有静态上下文都是共享的,无法为不同的类型参数提供不同的实现
例外情况: 静态方法可以定义自己的泛型参数:
public static <E> void staticMethod(E param) {
// 合法的静态泛型方法
}
(四)泛型类不能继承 Exception 或 Throwable
Java 禁止泛型类继承 Throwable 及其子类(如 Exception)。
错误示例:
// 编译错误
public class GenericException<T> extends Exception {
}
根本原因:
- Java 异常处理机制依赖编译时的类型检查
- 泛型类型在运行时会被擦除
- catch 块需要确切的异常类型
- 允许泛型异常会导致类型系统出现漏洞
替代方案:
- 对特定异常类型创建具体子类
- 在方法签名中使用通配符或边界限制
(五)注意泛型方法与泛型类的区别
泛型方法和泛型类是不同的概念,需要明确区分。
关键区别:
- 泛型方法可以在普通类或泛型类中定义
- 泛型方法的类型参数作用域仅限于该方法
- 泛型类的类型参数作用于整个类
示例分析:
public class GenericMethodVsClass<T> {
// 这不是泛型方法,使用了类的类型参数T
public void method1(T value) {
System.out.println(value);
}
// 这是泛型方法,类型参数E与类的类型参数T无关
public <E> void method2(E value) {
System.out.println(value);
}
}
使用场景:
- 当方法逻辑独立于类类型参数时使用泛型方法
- 当整个类需要统一类型参数时使用泛型类
- 它们可以组合使用,提供更灵活的类型处理能力

1494

被折叠的 条评论
为什么被折叠?



