如何在Java程序中使用泛型


如何在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 integerList = new ArrayList<>();

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

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

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

Java集合框架中的泛型

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

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

List和ArrayList的泛型应用

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

public interface List extends SequencedCollection { … }

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

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

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

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

Set和HashSet的泛型应用

Set接口与List类似:

public interface Set extends Collection { … }

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

Set 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值能被添加至集合,防止因错误类型转换引发的运行时错误。

Map和HashMap的泛型应用

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

public interface Map { … }

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

Map 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 {
    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 integerBox = new Box<>(123);
        System.out.println("整数盒内容:" + integerBox.getContent());


        // 创建存储String的Box
        Box 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类包含泛型值。如需更多泛型参数,可扩展为等,代码仍可正常编译。

Pair类示例:

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

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

让我们再看一个示例。

方法级泛型声明

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

public class GenericMethodDemo {


    // 声明泛型类型并打印指定类型数组
    public static  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时,若实际为其他类型将导致运行时错误。

泛型知识要点回顾

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

泛型是什么?为何使用?

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

Java集合中的泛型

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

泛型使用优势

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

相关文章

如何理解java基础中的Reference和引用类型?

首先要大致了解 Java 的几种引用类型。如下图所示,JDK 1.2 之后新增了 Reference 的概念,给开发人员提供了与 GC 交互的一种渠道。《深入理解 Java 虚拟机》中对于几种引用类型...

java的四种引用

java 中的引用分为 4种1.强引用 引用存在就不会被GC *2.软引用 heap memory(堆内存)满了就会被GC掉 *3.弱引用 每次GC就会回收掉(应用有:ThreadLocal) *4....

详细介绍一下Java中的什么是值传递?什么是引用传递?

Java中的参数传递是通过值传递进行的,即使是对象也是遵循这个规则,想要了解这个原理,首先我们就需要了解什么是值传递,什么是引用传递?值传递值传递是在调用方法的时候,方法接收到的参数是实际参数的一个副...

Java 值传递详解

开始之前,我们先来搞懂下面这两个概念:形参&实参值传递&引用传递形参&实参方法的定义可能会用到 参数(有参的方法),参数在程序语言中分为:实参(实际参数,Arguments):用于传递给函数/方法的参...

Java开发者必知的15个核心概念,第8个让你恍然大悟!

你是不是经常在面试中被问到Java的基础概念,却总是回答得不够全面?别担心,今天我们就来聊聊Java开发中那些必须掌握的核心知识点,帮你轻松应对各种技术面试! 1. ClassLoader:Java类...

java注解的原理,作用,特性和使用方法

Java 注解1. 原理Java注解是一种在源代码级别添加元数据的方式,其处理过程分为三个主要步骤:编译时处理:注解信息由编译器读取并生成字节码中的属性。例如,@Override用于检查方法是否正确重...