头图

如何在Java程序中使用泛型

泛型可以使你的代码更灵活、更易读,并能帮助你在运行时避免ClassCastExceptions。让我们通过这篇结合Java集合框架的泛型入门指南,开启你的泛型之旅。

Java 5引入的泛型增强了代码的类型安全性并提升了可读性。它能帮助你避免诸如ClassCastException(当尝试将对象强制转换为不兼容类型时引发的异常)这类运行时错误。

本教程将解析泛型概念,通过三个结合Java集合框架的实例演示其应用。同时我们将介绍原始类型(raw types),探讨选择使用原始类型而非泛型的场景及其潜在风险。

Java编程中的泛型

  • 为何使用泛型?
  • 如何利用泛型保障类型安全
  • Java集合框架中的泛型应用
  • Java泛型类型示例
  • 原始类型与泛型对比

为何使用泛型?

泛型在Java集合框架中被广泛用于java.util.List、java.util.Set和java.util.Map等接口。它们也存在于Java其他领域,如java.lang.Class、java.lang.Comparable 和java.lang.ThreadLocal。

在泛型出现前,Java代码常缺乏类型安全保障。以下是非泛型时代Java代码的典型示例:

List integerList = new ArrayList();
integerList.add(1);
integerList.add(2);
integerList.add(3);


for (Object element : integerList) {
    Integer num = (Integer) element; // 必须显式类型转换
    System.out.println(num);
}

这段代码意图存储Integer对象,但没有任何机制阻止你添加其他类型(如字符串):

integerList.add("Hello");

当尝试将String强制转换为Integer时,这段代码会在运行时抛出ClassCastException。

利用泛型保障类型安全

为解决上述问题并避免ClassCastExceptions,我们可以使用泛型指定列表允许存储的对象类型。此时无需手动类型转换,代码更安全且更易理解:

List<Integer> integerList = new ArrayList<>();

integerList.add(1);
integerList.add(2);
integerList.add(3);

for (Integer num : integerList) {
    System.out.println(num);
}

List<Integer>表示"存储Integer对象的列表"。基于此声明,编译器确保只有Integer对象能被添加至列表,既消除了类型转换需求,也预防了类型错误。

Java集合框架中的泛型

泛型深度集成于Java集合框架,提供编译时类型检查并消除显式类型转换需求。当使用带泛型的集合时,你需指定集合可容纳的元素类型。Java编译器基于此规范确保你不会意外插入不兼容对象,从而减少错误并提升代码可读性。

为演示泛型在Java集合框架中的使用,让我们观察几个实例。

List和ArrayList的泛型应用

前例已简要展示ArrayList的基本用法。现在让我们通过List接口的声明深入理解这一概念:

public interface List<E> extends SequencedCollection<E> { … }

此处声明泛型变量为"E",该变量可被任何对象类型替代。注意变量E代表元素(Element)。

接下来演示如何用具体类型替换<E>变量。下例中将<E>替换为<String>:

List<String> list = new ArrayList<>();
list.add("Java");
list.add("Challengers");
// list.add(1); // 此行会导致编译时错误

List<String>声明该列表仅能存储String对象。如代码最后一行所示,尝试添加Integer将引发编译错误。

Set和HashSet的泛型应用

Set接口与List类似:

public interface Set<E> extends Collection<E> { … }

我们将用<Double>替换<E>,使集合只能存储Double值:

Set<Double> doubles = new HashSet<>();
doubles.add(1.5);
doubles.add(2.5);
// doubles.add("three"); // 编译时错误


double sum = 0.0;
for (double d : doubles) {
    sum += d;
}

Set<Double>确保只有Double值能被添加至集合,防止因错误类型转换引发的运行时错误。

Map和HashMap的泛型应用

我们可以声明任意数量的泛型类型。以键值数据结构Map为例,K代表键(Key),V代表值(Value):

public interface Map<K, V> { … }

现在用String替换K作为键类型,用Integer替换V作为值类型:

Map<String, Integer> map = new HashMap<>();
map.put("Duke", 30);
map.put("Juggy", 25);
// map.put(1, 100); // 此行会导致编译时错误

此例展示将String键映射到Integer值的HashMap。添加Integer类型的键将不被允许并导致编译错误。

泛型命名规范

我们可以在任何类中声明泛型类型。虽然可以使用任意名称,但建议遵循命名规范:

  • E 代表元素(Element)
  • K 代表键(Key)
  • V 代表值(Value)
  • T 代表类型(Type)

应避免使用无意义的"X"、"Y"或"Z"等名称。

Java泛型类型使用示例

现在通过更多示例深入演示Java中泛型类型的声明与使用。

创建通用对象容器

我们可以在自定义类中声明泛型类型,不必局限于集合类型。下例中,Box类通过声明泛型类型E来操作任意元素类型。注意泛型类型E声明于类名之后,随后即可作为属性、构造器、方法参数和返回类型使用:

// 定义带泛型参数E的Box类
public class Box<E> {
    private E content; // 存储E类型对象


    public Box(E content) { this.content = content; }
    public E getContent() { return content; }
    public void setContent(E content) { 
        this.content = content;
    }


    public static void main(String[] args) {
        // 创建存储Integer的Box
        Box<Integer> integerBox = new Box<>(123);
        System.out.println("整数盒内容:" + integerBox.getContent());


        // 创建存储String的Box
        Box<String> stringBox = new Box<>("Hello World");
        stringBox.setContent("Java Challengers");
        System.out.println("字符串盒内容:" + stringBox.getContent());
    }
}

输出结果:

整数盒内容:123
字符串盒内容:Java Challengers

代码要点:

  • Box类使用类型参数E作为容器存储对象的占位符,允许Box处理任意对象类型
  • 构造器初始化Box实例时接受指定类型对象,确保类型安全
  • getContent返回与实例创建时指定的泛型类型匹配的对象,无需类型转换
  • setContent通过类型参数E确保只能设置正确类型的对象
  • main方法创建了存储Integer和String的Box实例
  • 每个Box实例操作特定数据类型,展现泛型在类型安全方面的优势

此例展示了Java泛型的基础实现,演示了如何以类型安全方式创建和操作任意类型对象。

处理多数据类型

我们可以声明多个泛型类型。以下Pair类包含<K, V>泛型值。如需更多泛型参数,可扩展为<K, V, V1, V2, V3>等,代码仍可正常编译。

Pair类示例:

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; }
}


public class GenericsDemo {
    public static void main(String[] args) {
        Pair<String, Integer> person = new Pair<>("Duke", 30);


        System.out.println("姓名:" + person.getKey());
        System.out.println("年龄:" + person.getValue());


        person.setValue(31);
        System.out.println("更新后年龄:" + person.getValue());
    }
}

输出结果:

姓名:Duke
年龄:30
更新后年龄:31

代码要点:

  • Pair<K, V>类包含两个类型参数,适用于任意数据类型组合
  • 构造器与方法使用类型参数实现严格类型检查
  • 创建存储String(姓名)和Integer(年龄)的Pair对象
  • 访问器和修改器方法操作Pair数据
  • Pair类可存储管理关联信息而不受特定类型限制,展现泛型的灵活性与强大功能

此例展示泛型如何创建支持多数据类型的可复用类型安全组件,提升代码复用性和可维护性。

让我们再看一个示例。

方法级泛型声明

泛型类型可直接在方法中声明,无需在类级别定义。若某个泛型类型仅用于特定方法,可在方法签名返回类型前声明:

public class GenericMethodDemo {


    // 声明泛型类型<T>并打印指定类型数组
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }


    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4};
        printArray(intArray);


        String[] stringArray = {"Java", "Challengers"};
        printArray(stringArray);
    }
}

输出结果:

1 2 3 4
Java Challengers

原始类型与泛型对比

原始类型指未指定类型参数的泛型类或接口名称。在Java 5引入泛型前,原始类型被广泛使用。现今开发者通常仅在与遗留代码兼容或与非泛型API交互时使用原始类型。即使使用泛型,仍需了解如何识别和处理原始类型。

典型原始类型示例——未指定类型参数的List声明:

 List rawList = new ArrayList();

此处List rawList声明了一个未指定泛型参数的列表。rawList可存储任意类型对象(Integer、String、Double等)。由于未指定类型,编译器不会对添加至列表的对象类型进行检查。

使用原始类型的编译警告

Java编译器会对原始类型使用发出警告,提醒开发者可能存在的类型安全隐患。当使用泛型时,编译器会检查集合(如List、Set)中存储的对象类型、方法返回类型和参数是否匹配声明类型,从而预防如ClassCastException的常见错误。

使用原始类型时,由于未指定存储对象类型,编译器无法进行类型检查,因此会发出警告提示你绕过了泛型提供的类型安全机制。

编译警告示例

以下代码演示编译器如何对原始类型发出警告:

List list = new ArrayList(); // 警告:原始使用参数化类'List'
list.add("hello");
list.add(1);

编译时通常会显示:

注意:SomeFile.java使用了未经检查或不安全的操作。
注意:使用-Xlint:unchecked重新编译以获取详细信息。

使用-Xlint:unchecked参数编译将显示更详细警告:

warning: [unchecked] unchecked call to add(E) as a member of the raw type List
    list.add("hello");
            ^
  where E is a type-variable:
    E extends Object declared in interface List

若确信使用原始类型不会引入风险,或处理无法重构的遗留代码,可使用@SuppressWarnings("unchecked")注解抑制警告。但需谨慎使用,避免掩盖真实问题。

使用原始类型的后果

尽管原始类型有助于向后兼容,但存在两大缺陷:类型安全性缺失和维护成本增加。

  • 类型安全性缺失:泛型的核心优势是类型安全,使用原始类型将丧失这一优势。编译器不进行类型正确性检查,可能导致运行时ClassCastException。
  • 维护成本增加:使用原始类型的代码缺乏泛型提供的明确类型信息,维护难度加大,易产生仅在运行时暴露的错误。

类型安全问题示例:使用原始类型List而非泛型List<String>时,编译器允许添加任意类型对象。当从列表检索元素并尝试强制转换为String时,若实际为其他类型将导致运行时错误。

泛型知识要点回顾

泛型以高度灵活性提供类型安全保障。以下回顾关键要点:

泛型是什么?为何使用?

  • code.Java 5引入泛型以提升代码类型安全性和灵活性
  • 主要优势在于帮助避免ClassCastException等运行时错误
  • 泛型广泛应用于Java集合框架,也见于Class、Comparable、ThreadLocal等组件
  • 通过阻止不兼容类型插入实现类型安全

Java集合中的泛型

  • List和ArrayList:List<E>允许指定元素类型E,确保列表类型专一
  • Set和HashSet:Set<E>限定元素为类型E,保持一致性
  • Map和HashMap:Map<K,V>定义键值类型,提升类型安全性和代码清晰度

泛型使用优势

  • 通过阻止不兼容类型插入减少错误
  • 明确类型关联提升代码可读性和可维护性
  • 便于以类型安全方式创建和管理集合等数据结构


信码由缰
65 声望8 粉丝

“码”界老兵,分享程序人生。