Day23 | Java泛型详解

今天我们一起学习一个Java中非常核心且强大的特性--泛型。

泛型是Java SE 5.0引入的,它的出现极大地增强了代码的可读性和安全性。

一、为什么需要泛型

在泛型出现之前,我们一般使用Object类来实现通用代码。

Object是所有Java类的根类,所以它可以引用任何类型的对象。

这样做虽然灵活,但也带来了两个显而易见的麻烦。

1、强制类型转换

当你从一个集合(比如ArrayList)中取出一个元素时,它的类型是Object。

你必须手动把它强制转换成你所期望的真实类型,才能调用它特有的方法。

没有泛型的时候,都是这样写代码:

java 复制代码
package com.lazy.snail.day23;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName Day23Demo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/11 9:35
 * @Version 1.0
 */
public class Day23Demo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("Hello");
        list.add(123);

        String a1 = (String) list.get(0);

        Integer a2 = (Integer) list.get(1);

        String a3 = (String) list.get(1);
    }
}

集合里面装的都是Object(什么都能装),但是你取的时候就得知道你每个位置装的是什么东西。

如果你类型转换错误了,程序直接就崩溃了。

在编译期是没有任何问题,运行时问题才暴露出来。

你想想看,假设出现了这种问题,代码都在线上运行了才暴露问题,谁受得了。

2、缺乏类型安全

上面的例子里,你可以往ArrayList放任何东西。编译器表示很无奈,它没办法在编译阶段检查出你是不是添加了错误类型的对象。

所有的风险都被推迟到了运行时,你就说这种代码脆不脆弱吧。

泛型就是为了解决两个痛点:

泛型允许你在编译时就指定集合(或类、方法)里能存放的对象类型。

编译器会帮你检查,如果放了错误的类型,会直接报错。

然后类型一旦明确,你从泛型集合里获取元素时,就不再需要进行强制类型转换了。

把上面的例子改成这样:

java 复制代码
package com.lazy.snail.day23;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName Day23Demo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/11 9:35
 * @Version 1.0
 */
public class Day23Demo {
    public static void main(String[] args) {
        List<String> list = new ArrayList();
        list.add("Hello");
        list.add("lazysnail");
        list.add(123);

        String a1 = list.get(0);
        String a2 = list.get(1);
    }
}

你限定了集合只能放String类型,当你放123进去,直接就编译错误了。

然后你在获取集合元素的时候,也不再需要强制类型转换了。

二、泛型怎么用

主要有三种应用形式:

2.1 泛型类

定义一个类的时候,可以在类名后面加上,T是一个类型参数。

这个T可以理解成一个占位符,表示将来使用这个类时会传入的实际类型。

java 复制代码
package com.lazy.snail.day23;

/**
 * @ClassName Box
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/11 15:39
 * @Version 1.0
 */
public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();
        integerBox.setItem(2025);
        Integer year = integerBox.getItem();
        System.out.println("年份: " + year);

        Box<String> stringBox = new Box<>();
        stringBox.setItem("懒惰蜗牛");
        String nickName = stringBox.getItem();
        System.out.println("昵称: " + nickName);
    }
}

代码里的T只是一个约定俗成的名称,代表"Type"。你可以换成其他字母,效果都一样。

2.2 泛型接口

泛型接口跟泛型类定义很像,也是在接口名后添加类型参数。

java 复制代码
package com.lazy.snail.day23;

import java.util.Random;

/**
 * @ClassName Generator
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/11 15:47
 * @Version 1.0
 */
public interface Generator<T> {
    T generate();
}

class StringGenerator implements Generator<String> {

    @Override
    public String generate() {
        return "字符串生成器";
    }
}

class RandomNumberGenerator<T extends Number> implements Generator<T> {
    private T[] numbers;

    public RandomNumberGenerator(T[] numbers) {
        this.numbers = numbers;
    }

    @Override
    public T generate() {
        if (numbers == null || numbers.length == 0) {
            return null;
        }
        Random random = new Random();
        int index = random.nextInt(numbers.length);
        return numbers[index];
    }
}

StringGenerator类在实现Generator的时候,限定了类型参数是String。

StringGenerator中实现方法generate()的返回值就确定为String类型。

RandomNumberGenerator类本身也是个泛型类,T extends Number表示只能接受Number及其子类。

它在实现Generator接口的时候,把自身的泛型类型T传递给了接口Generator<T>,还对T增加了extends Number的类型约束。

泛型的上下限,我们会在后文补充讲。

2.3 泛型方法

有的时候,我们只是希望某个方法具有泛型能力,而不是整个类。

这个时候就可以定义泛型方法。

泛型方法可以在普通类、泛型类或者接口里定义。

java 复制代码
package com.lazy.snail.day23;

/**
 * @ClassName UtilDemo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/11 16:09
 * @Version 1.0
 */
public class UtilDemo {
    public static <T> void printArray(T[] inputArray) {
        for(T element : inputArray) {
            System.out.printf("%s ", element);
        }
    }

    public static void main(String[] args) {
        Integer[] intArray = { 1, 2, 3, 4, 5 };
        String[] stringArray = { "懒惰", "蜗牛" };

        UtilDemo.printArray(intArray);
        System.out.println();
        UtilDemo.printArray(stringArray);
    }
}

泛型方法的类型参数列表(<T>)要放在返回值类型之前。

这个声明就是告诉编译器,这是一个泛型方法,并且这个方法里使用的T是这个方法的类型参数,而不是类的。

Java编译器可以根据传入的参数类型,自动推断出泛型方法中的T具体是什么类型。

2.4 容易混淆的"泛型方法"

回看2.1节示例代码里的getItem方法,虽然返回值是泛型类,但方法本身并不是泛型方法。

getItem中的T并不是自己声明的,只是借用了Box定义的时候已经声明的类型参数T。

这个方法的类型T在创建Box对象时就已经确定了。

它不具备在调用时根据参数推断类型的泛型方法特性。

还有一种参数是泛型类,但方法本身不是泛型方法,比如:

java 复制代码
public class BoxProcessor {
    public void inspect(Box<Integer> box) {
        System.out.println(box.getItem());
    }
}

这个方法的参数是Box<Integer>,一个具体化了的泛型类。不是一个可变的类型占位符。

完全不具备泛型的通用性,应该一眼就看出它不是什么泛型方法。

把它改成这样:

java 复制代码
public class BoxProcessor {
    public <T> void inspect(Box<T> box) {
        System.out.println(+ box.getItem());
    }
}

声明了这是一个泛型方法,T是一个临时的类型参数。可以用它来处理任何类型的Box。

这才成了真正的泛型方法。

其实判断一个方法是不是泛型方法,就看一点就够了。 方法签名里,紧跟在修饰符(public, static等)之后、返回值类型之前,有没有、 这样的类型参数声明? 有,妥妥的泛型方法。 没有,不管它的参数或返回值是不是T,都不是泛型方法。只是在使用它所属的类或接口已经声明好的泛型类型而已。

结语

今天,我们简单探讨了Java的泛型。

大致了解了Java是怎么通过泛型来保证类型安全和简化代码,怎么把运行时的风险提前到了编译期。

通过案例大致的了解了泛型类、泛型接口和泛型方法这三板斧的基本用法。

下一篇预告

Day24 | Java泛型通配符与边界解析

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

相关推荐
幌才_loong29 分钟前
.NET8+Autofac 实战宝典:从组件拆解到场景落地的依赖注入新范式
后端·.net
onejson29 分钟前
idea中一键执行maven和应用重启
java·maven·intellij-idea
CoderYanger30 分钟前
动态规划算法-简单多状态dp问题:13.删除并获得点数
java·开发语言·数据结构·算法·leetcode·动态规划·1024程序员节
听风吟丶31 分钟前
Java 微服务 APM 实战:Prometheus+Grafana 构建全维度性能监控与资源预警体系
java·微服务·prometheus
ohnoooo934 分钟前
C++ STL库常用容器函数
java·开发语言
天骄t36 分钟前
深入解析栈:数据结构与系统栈
java·开发语言·数据结构
源代码•宸36 分钟前
GoLang并发示例代码1(关于逻辑处理器运行顺序)
开发语言·经验分享·后端·golang
CoderYanger38 分钟前
A.每日一题——3625. 统计梯形的数目 II
java·算法·leetcode·职场和发展
Dolphin_Home38 分钟前
接口字段入参出参分离技巧:从注解到DTO分层实践
java·spring boot·后端