0%

泛型

Java 泛型详解:类型安全的基石

泛型(Generics)是 Java 5 引入的核心特性,它允许在定义类、接口和方法时使用类型参数,从而实现代码的类型安全复用性。在泛型出现之前,集合类只能存储 Object 类型,取出时必须强制转换,容易引发运行时 ClassCastException。泛型的出现将类型检查提前到编译期,从根本上解决了这一问题。

泛型的核心价值

  1. 编译期类型检查:阻止将错误类型的对象放入集合,避免运行时类型转换异常。
  2. 消除强制类型转换:从集合中获取元素时无需手动转换,代码更简洁。
  3. 代码复用:一套泛型代码可适配多种数据类型(如 ArrayList<String>ArrayList<Integer> 共享同一套 ArrayList 实现)。
  4. 提升可读性:通过类型参数明确集合或方法支持的数据类型,代码意图更清晰。

泛型的基本用法

泛型类与泛型接口

泛型类 / 接口在定义时声明类型变量(用 <T> 表示),使用时指定具体类型。

示例:自定义泛型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义泛型类,T为类型变量(可替换为任意标识符,通常用T、E、K、V等)
public class Box<T> {
private T value; // 使用类型变量作为属性类型

public Box(T value) { // 作为构造器参数类型
this.value = value;
}

public T getValue() { // 作为返回值类型
return value;
}

public void setValue(T value) { // 作为方法参数类型
this.value = value;
}
}

// 使用泛型类:指定具体类型(如String、Integer)
public class BoxDemo {
public static void main(String[] args) {
Box<String> stringBox = new Box<>("Hello");
String str = stringBox.getValue(); // 无需强制转换

Box<Integer> intBox = new Box<>(123);
int num = intBox.getValue(); // 自动拆箱
}
}
示例:泛型接口
1
2
3
4
5
6
7
8
9
10
11
12
// 定义泛型接口
public interface Generator<T> {
T generate(); // 抽象方法,返回值为类型T
}

// 实现泛型接口时指定具体类型
public class NumberGenerator implements Generator<Integer> {
@Override
public Integer generate() {
return (int) (Math.random() * 100);
}
}

泛型方法

泛型方法是在方法声明中包含类型变量的方法,可独立于泛型类存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GenericMethodDemo {
// 泛型方法:<T> 声明类型变量,T作为参数和返回值类型
public static <T> T getFirstElement(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[0];
}

public static void main(String[] args) {
String[] strArray = {"a", "b", "c"};
String firstStr = getFirstElement(strArray); // 自动推断类型为String

Integer[] intArray = {1, 2, 3};
Integer firstInt = getFirstElement(intArray); // 自动推断类型为Integer
}
}
  • 类型推断:调用泛型方法时,编译器会根据传入的参数自动推断类型变量 T 的具体类型,无需显式指定(如 getFirstElement(strArray) 无需写成 getFirstElement<String>(strArray))。

常用类型变量命名规范

为增强可读性,Java 约定了常用的类型变量标识符:

  • T(Type):表示任意类型
  • E(Element):表示集合中的元素类型
  • K(Key):表示映射中的键类型
  • V(Value):表示映射中的值类型
  • N(Number):表示数值类型

类型变量的限定(Bounds)

泛型允许通过 extendssuper 关键字限制类型变量的范围,称为类型限定,用于约束泛型可接受的类型。

上限限定(extends

<T extends A> 表示类型变量 T 必须是 A 类的子类(或 A 本身),或实现了 A 接口(A 为接口时)。

示例:限定类型必须实现 Comparable 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 类型变量T必须实现Comparable接口(上限限定)
public static <T extends Comparable<T>> T findMax(T[] array) {
if (array == null || array.length == 0) {
return null;
}
T max = array[0];
for (T element : array) {
if (element.compareTo(max) > 0) { // 因限定了Comparable,可安全调用compareTo方法
max = element;
}
}
return max;
}

public static void main(String[] args) {
Integer[] numbers = {3, 1, 4, 1, 5};
System.out.println(findMax(numbers)); // 输出:5(Integer实现了Comparable)

String[] words = {"apple", "banana", "cherry"};
System.out.println(findMax(words)); // 输出:cherry(String实现了Comparable)
}
  • 多边界限定:可指定多个上限,用&分隔(类必须放在第一个位置)。

    1
    2
    // T必须是Number的子类且实现了Comparable接口
    <T extends Number & Comparable<T>>

下限限定(super

<? super T> 表示类型变量必须是 T 类的父类(或 T 本身),通常用于限制方法参数的类型。

示例:下限限定的应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

public class SuperBoundDemo {
// 向列表中添加T类型或其子类型的元素(使用super限定)
public static <T> void addElements(List<? super T> list, T... elements) {
for (T element : elements) {
list.add(element);
}
}

public static void main(String[] args) {
List<Object> objList = new ArrayList<>();
addElements(objList, "a", "b"); // 正确:Object是String的父类

List<Number> numList = new ArrayList<>();
addElements(numList, 1, 2.5); // 正确:Number是Integer和Double的父类
}
}

通配符(Wildcards)

通配符 ? 用于表示 “未知类型”,通常在泛型类 / 方法的参数中使用,解决泛型的类型不兼容问题。

无界通配符(<?>

<?> 表示任意类型,适用于不依赖具体类型的场景(如判断集合是否为空)。

1
2
3
4
5
6
7
8
9
10
public static boolean isEmpty(List<?> list) {
return list == null || list.size() == 0;
}

public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(isEmpty(strList)); // true
System.out.println(isEmpty(intList)); // true
}

上界通配符(<? extends T>

<? extends T> 表示未知类型,但必须是 T 的子类(或 T 本身),适用于读取操作(不能写入,除了 null)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 读取列表中的元素(上界通配符)
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue(); // 可安全调用Number的方法
}
return sum;
}

public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
System.out.println(sumOfList(ints)); // 6.0
System.out.println(sumOfList(doubles)); // 4.0
}
  • 限制:不能向上界通配符的集合中添加元素(除null),因为编译器无法确定具体类型。

    1
    2
    List<? extends Number> list = new ArrayList<Integer>();
    list.add(1); // 编译报错:无法确定list是否接受Integer

下界通配符(<? super T>

<? super T> 表示未知类型,但必须是 T 的父类(或 T 本身),适用于写入操作(读取时只能返回 Object)。

1
2
3
4
5
6
7
8
9
10
11
12
// 向下界通配符的集合中添加元素
public static void addIntegers(List<? super Integer> list) {
list.add(1); // 正确:Integer及其父类(如Number、Object)均可接受Integer
list.add(2);
}

public static void main(String[] args) {
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();
addIntegers(nums); // 正确:Number是Integer的父类
addIntegers(objs); // 正确:Object是Integer的父类
}

泛型的局限性

  1. 不能使用基本类型实例化泛型:泛型类型参数必须是引用类型,如 List<int> 不合法,需使用 List<Integer>

  2. 运行时类型擦除:泛型信息在编译后会被擦除(类型变量被替换为上限类型,默认为 Object),因此运行时无法获取泛型的具体类型。

    1
    2
    List<String> list = new ArrayList<>();
    System.out.println(list.getClass() == ArrayList.class); // true(运行时无泛型信息)
  3. 不能创建泛型数组new T[10] 不合法,因类型擦除导致数组无法确定元素类型。

  4. 不能在静态上下文中使用泛型类型变量:泛型类的静态方法或静态属性不能引用类的类型变量(类型变量属于实例)。

最佳实践

  1. 始终指定泛型类型:避免使用原始类型(如 List 而非 List<String>),否则失去泛型的类型安全优势。
  2. 优先使用具体类型而非通配符:如 List<String>List<?> 更清晰,仅在需要灵活性时使用通配符。
  3. 合理选择上下界:读取操作用上界通配符(<? extends T>),写入操作用下界通配符(<? super T>)。
  4. 避免过度泛型化:仅在需要适配多种类型时使用泛型,简单场景无需引入泛型增加复杂度

欢迎关注我的其它发布渠道