Java泛型进阶:从基础到高级特性完全指南

一、泛型基础回顾

1.1 什么是泛型?

泛型是JDK 1.5引入的新语法,用通俗的话来说,泛型就是适用于许多许多类型 。从代码实现的角度看,泛型是对类型实现了参数化

核心价值:传统的类和方法只能使用具体的类型(基本类型或自定义类),而泛型使得我们可以编写可以应用于多种类型的代码,极大地提高了代码的复用性。

1.2 为什么需要泛型?

一个示例说明了泛型的重要性:

复制代码
// 没有泛型的实现
class MyArray {
    public Object[] array = new Object[10];
    public Object getPos(int pos) {
        return this.array[pos];
    }
    public void setVal(int pos, Object val) {
        this.array[pos] = val;
    }
}

这种实现方式存在两个问题:

  1. 任何类型的数据都可以存放

  2. 获取数据时需要强制类型转换,容易出错

泛型的解决方案

复制代码
class MyArray<T> {
    public T[] array = (T[]) new Object[10];
    public T getPos(int pos) {
        return this.array[pos];
    }
    public void setVal(int pos, T val) {
        this.array[pos] = val;
    }
}

使用泛型后,代码变得更加类型安全:

  1. 只能存储指定类型的数据

  2. 不需要强制类型转换

  3. 编译时进行类型检查

二、泛型类与泛型方法

2.1 泛型类的定义

语法

复制代码
class 泛型类名称<类型形参列表> {
    // 这里可以使用类型参数
}

示例

复制代码
class MyArray<T> {
    // 使用类型参数T
}

类型形参的命名规范(文档中提到的约定):

  • E 表示 Element

  • K 表示 Key

  • V 表示 Value

  • N 表示 Number

  • T 表示 Type

  • S, U, V 等表示第二、第三、第四个类型

2.2 泛型类的使用

基本使用

复制代码
MyArray<Integer> list = new MyArray<Integer>();

类型推导

复制代码
MyArray<Integer> list = new MyArray<>();  // 编译器可以推导出类型为Integer

重要限制:泛型只能接受类,所有基本数据类型必须使用包装类。

2.3 裸类型(了解概念)

裸类型是指泛型类没有指定类型实参的情况:

复制代码
MyArray list = new MyArray();  // 裸类型

注意:不应该自己使用裸类型,它只是为了兼容老版本的API而保留的机制。

2.4 泛型方法

语法

复制代码
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }

示例

复制代码
public class Util {
    // 静态的泛型方法需要在static后用<>声明泛型类型参数
    public static <E> void swap(E[] array, int i, int j) {
        E t = array[i];
        array[i] = array[j];
        array[j] = t;
    }
}

使用方式

复制代码
// 使用类型推导
Integer[] a = {...};
swap(a, 0, 9);

// 不使用类型推导
Util.<Integer>swap(a, 0, 9);

三、泛型的实现原理

3.1 类型擦除机制

Java泛型是通过类型擦除机制在编译级别实现的。这是泛型实现中最核心、也最容易被误解的概念。

关键点

  1. 在编译过程中,所有的泛型类型参数T都会被替换为Object

  2. 编译器生成的字节码在运行期间并不包含泛型的类型信息

  3. 这是Java泛型与C++模板的最大区别

示例

复制代码
// 源代码
class MyArray<T> {
    public T[] array = (T[]) new Object[10];
}

// 编译后(概念上)
class MyArray {
    public Object[] array = new Object[10];
}

3.2 为什么不能实例化泛型类型数组?

问题 :为什么T[] ts = new T[5];是不对的?

原因 :类型擦除后,这相当于Object[] ts = new Object[5],然后尝试赋值给T[]引用,但数组是协变的,可能会导致类型安全问题。

以下示例说明了问题:

复制代码
class MyArray<T> {
    public T[] array = (T[]) new Object[10];
    public T[] getArray() {
        return array;
    }
}

// 使用时可能出现问题
MyArray<String> myArray = new MyArray<>();
// ...
Object[] objects = myArray.getArray();  // 返回的Object数组可能包含任何类型

正确的方式(了解即可):

复制代码
class MyArray<T> {
    public T[] array;
    public MyArray() {
        // 通过反射创建数组
    }
}

四、泛型的高级特性

4.1 泛型的上界

泛型上界用于对类型参数进行约束,指定类型参数必须是某个类或接口的子类。

语法

复制代码
class 泛型类名称<类型形参 extends 类型边界> { ... }

示例1:限制为Number的子类

复制代码
public class MyArray<E extends Number> { ... }

// 正确使用
MyArray<Integer> l1;  // Integer是Number的子类

// 编译错误
MyArray<String> l2;  // String不是Number的子类

示例2:限制为实现Comparable接口

复制代码
public class MyArray<E extends Comparable<E>> { ... }

注意 :如果没有指定类型边界E,可以视为E extends Object

4.2 通配符

通配符?用于在泛型的使用中表示未知类型,主要用于增加API的灵活性。

4.2.1 为什么需要通配符?

考虑以下场景:

复制代码
class Message<T> {
    private T message;
    public T getMessage() { return message; }
    public void setMessage(T message) { this.message = message; }
}

public class TestDemo {
    public static void main(String[] args) {
        Message<String> message = new Message<>();
        message.setMessage("比特就业课欢迎您");
        fun(message);
    }
    
    public static void fun(Message<String> temp) {
        System.out.println(temp.getMessage());
    }
}

如果现在有Message<Integer>类型,fun()方法将无法处理。通配符?解决了这个问题。

4.2.2 通配符上界

语法<? extends 上界>

示例

复制代码
class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}
class Banana extends Fruit {}

public static void fun(Message<? extends Fruit> temp) {
    // temp.setMessage(new Banana());  // 无法修改!
    // temp.setMessage(new Apple());    // 无法修改!
    Fruit b = temp.getMessage();        // 可以获取元素
    System.out.println(b);
}

重要特性

  • 通配符上界不能进行写入数据,只能进行读取数据

  • 因为编译器无法确定具体是哪个子类

4.2.3 通配符下界

语法<? super 下界>

示例

复制代码
class Plate<T> {
    private T plate;
    public T getPlate() { return plate; }
    public void setPlate(T plate) { this.plate = plate; }
}

public static void fun(Plate<? super Fruit> temp) {
    // 可以修改!添加的是Fruit或者Fruit的子类
    temp.setPlate(new Apple());    // Fruit的子类
    temp.setPlate(new Fruit());    // Fruit本身
    // Fruit fruit = temp.getPlate();  // 不能接收,无法确定是哪个父类
    System.out.println(temp.getPlate());  // 只能直接输出
}

重要特性

  • 通配符下界不能进行读取数据,只能写入数据

  • 可以写入指定类型或其子类的对象

五、PECS原则

我们可以总结出这个重要原则:

PECS:Producer Extends, Consumer Super

  • 如果需要一个提供(生产) ​ 数据的泛型容器,使用<? extends T>

  • 如果需要一个消费 数据的泛型容器,使用<? super T>

六、泛型的最佳实践

6.1 优先使用泛型

在编写可重用代码时,优先考虑使用泛型,这可以提高代码的类型安全性和复用性。

6.2 合理使用通配符

在API设计中,合理使用通配符可以提高API的灵活性,但要注意PECS原则。

6.3 避免泛型数组

由于Java泛型的擦除机制,尽量避免创建泛型数组,如果必须使用,要特别注意类型安全。

6.4 注意类型擦除的影响

由于类型擦除,以下操作是不允许的:

  • 不能使用instanceof检查泛型类型

  • 不能创建泛型类的数组

  • 不能抛出或捕获泛型类的异常

  • 不能重载参数类型擦除后相同的方法

七、总结

Java泛型是提高代码类型安全性和复用性的重要特性。通过本文的学习,我们应该掌握:

  1. 泛型的基本概念和使用:如何定义和使用泛型类、泛型方法

  2. 类型擦除原理:理解Java泛型的实现机制

  3. 通配符的使用 :掌握?? extends T? super T的区别和适用场景

  4. 泛型上界:如何约束类型参数的范围

泛型的主要优点

  1. 类型安全:编译时进行类型检查

  2. 消除强制类型转换

  3. 提高代码复用性

  4. 提高代码可读性

泛型的局限性(由于类型擦除):

  1. 不能使用基本类型作为类型参数

  2. 不能创建泛型数组

  3. 不能使用instanceof检查泛型类型

  4. 不能创建具体类型的泛型实例

掌握泛型及其高级特性,是成为Java高级开发者的重要一步。合理使用泛型可以使代码更加健壮、灵活,同时提高开发效率。希望本博客能帮助你深入理解Java泛型的各个方面。

相关推荐
He1955012 小时前
wordpress搭建块
开发语言·wordpress·古腾堡·wordpress块
建行一世2 小时前
【Windows笔记本大模型“傻瓜式”教程】使用LLaMA-Factory工具来完成对Windows笔记本大模型Qwen2.5-3B-Instruct微调
windows·ai·语言模型·llama
老天文学家了2 小时前
蓝桥杯备战Python
开发语言·python
赫瑞2 小时前
数据结构中的排列组合 —— Java实现
java·开发语言·数据结构
初夏睡觉3 小时前
c++1.3(变量与常量,简单数学运算详解),草稿公放
开发语言·c++
升职佳兴3 小时前
C盘爆满自救:3步无损迁移应用数据到E盘(含回滚)
c语言·开发语言
ID_180079054733 小时前
除了 Python,还有哪些语言可以解析 JSON 数据?
开发语言·python·json
周末也要写八哥4 小时前
多进程和多线程的特点和区别
java·开发语言·jvm
FreakStudio4 小时前
小作坊 GitHub 协作闭环:fork-sync-dev-pr-merge 实战指南
python·单片机·嵌入式·面向对象·电子diy