探秘 Java 泛型:从类型参数到边界限制与类型擦除

在 Java 编程中,大家或许都遭遇过令人头疼的ClassCastException,尤其是在处理如IntegerString等不同类型对象时。这个异常通常是由于将对象强制转换为错误的数据类型所导致的。不过,Java 中的泛型可以帮助我们解决这一问题。

为什么我们需要泛型?

让我们从一个简单的例子开始。我们首先将不同类型的对象添加到一个ArrayList中。然后打印它们的值。

java 复制代码
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0);
System.out.println("String: " + str);

这里,我们向ArrayList添加了一个String对象。由于代码是自己编写,我们清楚元素类型,但编译器并不知晓。所以从列表获取值时得到的是Object类型,必须进行显式强制转换。

java 复制代码
list.add(123);
String number = (String) list.get(1);
System.out.println("Number: " + number);

如果我们向这个列表中添加一个Integer并尝试获取该值,我们将得到一个ClassCastException,因为Integer对象不能被强制转换为String。 而使用泛型,就能解决上述两个问题。使用菱形运算符明确指定列表中保存的对象类型,可实现编译时检查,无需显式强制转换。

java 复制代码
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 无需显式强制转换
System.out.println("String: " + str);
list.add(123); // 抛出编译时错误

类型参数命名约定

在前面示例中,List<String>的使用限制了列表可保存的对象类型。再看Box类处理不同类型数据的示例:

java 复制代码
public class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setValue("Hello, world!");
        System.out.println(stringBox.getValue());

        Box<Integer> integerBox = new Box<>();
        integerBox.setValue(123);
        System.out.println(integerBox.getValue());
    }
}

注意Box<T>类的声明,这里T是类型参数,表示Box类可处理该类型的任意对象。在main方法中创建Box<String>Box<Integer>实例,确保了类型安全。

根据官方文档,类型参数名称通常为单个大写字母。常见的类型参数名称有:

  • E - 元素(广泛用于 Java 集合框架)
  • K - 键
  • N - 数字
  • T - 类型
  • V - 值
  • SUV等 - 第二、第三、第四种类型

让我们看看如何编写一个泛型方法:

java 复制代码
public static <T> void printArray(T[] inputArr) {
    for (T element : inputArr) {
        System.out.print(element + " ");
    }
    System.out.println();
}

这里,我们接受任何类型的数组并打印其元素。请注意,你需要在方法返回类型之前的尖括号<>中指定泛型类型参数T。方法体遍历我们作为参数传递的任何类型T的数组,并打印每个元素。

java 复制代码
public static void main(String[] args) {
    // 创建不同类型的数组(Integer、Double和Character)
    Integer[] intArr = {1, 2, 3, 4, 5};
    Double[] doubleArr = {1.1, 2.2, 3.3, 4.4, 5.5};
    Character[] charArr = {'H', 'E', 'L', 'L', 'O'};

    System.out.println("Integer数组包含:");
    printArray(intArr);   // 传递一个Integer数组

    System.out.println("Double数组包含:");
    printArray(doubleArr);   // 传递一个Double数组

    System.out.println("Character数组包含:");
    printArray(charArr);   // 传递一个Character数组
}

我们可以通过传递不同类型的数组(IntegerDoubleCharacter)来调用这个泛型方法,你会看到你的程序将打印出这些数组的每个元素。

泛型的限制

在泛型中,我们使用边界来限制泛型类、接口或方法可以接受的类型。有两种类型:

1. 上界

这用于将泛型类型限制为上限。要定义上界,你使用extends关键字。通过指定上界,你确保类、接口或方法接受指定的类型及其所有子类。 语法如下:<T extends SuperClass>。例如,修改Box类:

java 复制代码
class Box<T extends Number> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

在这个例子中,T可以是任何扩展Number的类型,如IntegerDoubleFloat

2. 下界

这用于将泛型类型限制为下限。要定义下界,你使用super关键字。通过指定下界,你确保类、接口或方法接受指定的类型及其所有超类。 语法如下:<T super SubClass>。以下是使用下界的示例:

java 复制代码
public static void printList(List<? super Integer> list) {
    for (Object element : list) {
        System.out.print(element + " ");
    }
    System.out.println();
}

下界<? super Integer>的使用确保你可以将指定的类型及其所有超类(在这种情况下是IntegerNumberObject的列表)传递给printList方法。

什么是通配符?

你在上一个示例中看到的?被称为通配符。你可以使用它们来引用未知类型。你可以使用带有上界的通配符,在这种情况下它看起来像这样:<? extends Number>。它也可以与下界一起使用,如<? super Integer>

类型擦除

我们在类、接口或方法中使用的泛型类型仅在编译时可用,并且在运行时会被删除。这样做是为了确保向后兼容性,因为旧版本的Java(Java 1.5之前)不支持它。 编译器利用泛型类型信息确保类型安全。类型擦除过程如下:

  • 对于有界泛型类型,编译器会将其擦除为它的上界类型。例如,class Box<T extends Number>T会被擦除为Number

  • 对于无界泛型类型(如class Box<T>),T会被擦除为Object。所以在运行时,实际上并不能获取到泛型参数的具体类型信息。

java 复制代码
import java.util.ArrayList;
import java.util.List;
class GenericExample<T> {
    private List<T> list = new ArrayList<>();
    public void add(T element) {
        list.add(element);
    }
    public T get(int index) {
        return list.get(index);
    }
}

当编译器编译这段代码时,T会被擦除。对于add方法,实际上变成了类似public void add(Object element)(如果T是无界的)。对于get方法,返回值类型也被擦除为Object,不过编译器会在需要的时候插入强制类型转换。

结论

本文深入探讨了 Java 中的泛型概念及其使用方法,并给出了多个基本示例。理解和运用泛型能增强程序类型安全性,消除显式强制转换需求,使代码更具重用性和可维护性。希望通过本文的介绍,大家能在 Java 编程中更好地运用泛型,提升代码质量。

相关推荐
真是他几秒前
多继承出现的菱形继承问题
后端
Java技术小馆几秒前
SpringBoot中暗藏的设计模式
java·面试·架构
xiguolangzi1 分钟前
《springBoot3 中使用redis》
java
李菠菜4 分钟前
POST请求的三种编码及SpringBoot处理详解
spring boot·后端
李菠菜5 分钟前
浅谈Maven依赖传递中的optional和provided
后端·maven
李菠菜8 分钟前
非SpringBoot环境下Jedis集群操作Redis实战指南
java·redis
lqstyle8 分钟前
Redis的Set:你以为我是青铜?其实我是百变星君!
后端·面试
Piper蛋窝12 分钟前
Go 1.15 相比 Go 1.14 有哪些值得注意的改动?
后端
K8sCat19 分钟前
Golang与Kafka的五大核心设计模式
后端·kafka·go
不当菜虚困21 分钟前
JAVA设计模式——(四)门面模式
java·开发语言·设计模式