找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3207

积分

0

好友

414

主题
发表于 2026-2-12 16:47:48 | 查看: 36| 回复: 0

做 Java 开发肯定都用过 ArrayList 这类集合类,有没有遇到过这种情况?往集合里随便塞不同类型的数据,编译时没报错,运行起来却抛出 ClassCastException。比如把 IntegerString 混着放,取数据时强转就翻车了。

其实这个问题,用泛型(Generics)就能轻松解决。泛型说白了就是参数化类型,简单说就是给数据类型也加个参数,让代码在编译时就做好类型检查,不用等到运行时才报错。

一、为什么要用泛型

不用泛型的代码,就像开着没有安检的大门,什么类型的数据都能进,隐患重重:

// 没有泛型的集合,啥都能塞
ArrayList list = new ArrayList();
list.add("Java");
list.add(2024); // 不小心加了个Integer
// 遍历的时候强转,运行时直接报错
for (Object obj : list) {
    String str = (String) obj; // ClassCastException:Integer不能转String
}

用了泛型之后,编译时就会拦住非法数据,从根源上避免错误:

// 指定泛型为String,只能存字符串
ArrayList<String> list = new ArrayList<>();
list.add("Java");
list.add(2024); // 编译直接报错,根本过不了编译期
// 遍历不用强转,安全又省心
for (String str : list) {
    System.out.println(str);
}

这就是泛型的核心价值:编译时类型安全检查 + 避免繁琐的类型转换。除此之外,泛型还能让代码更通用,比如写一个方法既能处理 String,也能处理 Integer,不用重复写多份代码,这极大地提升了代码的复用性和设计质量。

二、泛型的 3 种核心用法:类、接口、方法

泛型的使用场景主要有 3 种,分别是泛型类、泛型接口、泛型方法,用法都很统一,记住 “声明类型参数 + 使用类型参数” 的套路就行。

1. 泛型类

泛型类就是在类定义时声明一个或多个类型参数(比如 <T>),在类的成员变量、方法中就能用这个类型参数当数据类型。

class 类名<类型参数> {
    private 类型参数 变量名; // 成员变量用类型参数
    public 类型参数 方法名() { // 返回值用类型参数
        return 变量名;
    }
}

常见的类型参数标识:T(通用类型)、E(集合元素)、K(键)、V(值),可以随便写,但大家约定俗成用这些字母,可读性更高。

// 泛型类,声明类型参数T
public class GenericBox<T> {
    private T value; // 成员变量类型为T
    // 构造方法,参数类型为T
    public GenericBox(T value) {
        this.value = value;
    }
    // 成员方法,返回值类型为T
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}
// 测试类
public class GenericTest {
    public static void main(String[] args) {
        // 创建String类型的GenericBox
        GenericBox<String> strBox = new GenericBox<>("Hello");
        String str = strBox.getValue(); // 不用强转,直接获取String
        System.out.println(str);
        // 创建Integer类型的GenericBox
        GenericBox<Integer> intBox = new GenericBox<>(100);
        Integer num = intBox.getValue(); // 直接获取Integer
        System.out.println(num);
    }
}

注意点

  • 泛型类的静态成员不能用类声明的类型参数(比如 static T value 会编译报错),因为静态成员在类加载时就初始化,而类型参数要在创建对象时才确定;
  • 可以声明多个类型参数,比如 class Pair<K, V> { private K key; private V value; },适合键值对这类场景。

2. 泛型接口

泛型接口和泛型类用法类似,在接口定义时声明类型参数,实现接口时确定具体类型。

// 泛型接口,声明类型参数T
public interface Generator<T> {
    T generate(); // 抽象方法,返回值类型为T
}
// 实现接口时,确定类型参数为String
public class StringGenerator implements Generator<String> {
    @Override
    public String generate() {
        return "随机字符串:" + Math.random();
    }
}
// 实现接口时,也可以保留类型参数,让实现类成为泛型类
public class NumberGenerator<T extends Number> implements Generator<T> {
    private T seed;
    public NumberGenerator(T seed) {
        this.seed = seed;
    }
    @Override
    public T generate() {
        return seed;
    }
}
// 测试
public class InterfaceTest {
    public static void main(String[] args) {
        Generator<String> strGen = new StringGenerator();
        System.out.println(strGen.generate());
        Generator<Integer> intGen = new NumberGenerator<>(10);
        System.out.println(intGen.generate());
    }
}

3. 泛型方法

泛型方法不用依赖泛型类,在方法声明时单独声明类型参数(<T>),只在当前方法中有效,灵活性更高。

关键是 <类型参数> 要写在返回值前面,这是泛型方法的标志。

public class GenericMethod {
    // 泛型方法,声明类型参数T
    public static <T> void print(T content) {
        System.out.println("内容:" + content + ",类型:" + content.getClass().getSimpleName());
    }
    public static void main(String[] args) {
        // 调用时不用指定类型,编译器自动推断
        print("Java泛型"); // 内容:Java泛型,类型:String
        print(123); // 内容:123,类型:Integer
        print(3.14); // 内容:3.14,类型:Double
    }
}

注意点

  • 泛型方法可以是静态的,这和泛型类的静态成员不同;
  • 可以声明多个类型参数,比如 <T, S> void method(T t, S s)

三、泛型通配符:? extends T? super T 怎么用?

这是泛型里最容易搞混的部分,其实记住 “PECS 原则” 就能搞定。先明确一个前提:List<Integer> 不是 List<Number> 的子类,虽然 IntegerNumber 的子类,但泛型不支持这种 “向上转型”,直接赋值会编译报错。

泛型通配符就是为了解决这个问题,主要有 3 种:<?>(无限定通配符)、<? extends T>(上界通配符)、<? super T>(下界通配符)。

1. 上界通配符:? extends T(只能读,不能写)

表示 “只能是 T 或 T 的子类”,比如 <? extends Number> 可以接收 IntegerFloatNumber 类型。

因为不确定集合里具体是 T 的哪个子类,添加元素可能导致类型不匹配,所以编译器禁止添加;但获取元素时,肯定是 T 类型,安全无风险。

public class ExtendsTest {
    // 接收Number及其子类的集合
    public static void printNumbers(List<? extends Number> list) {
        for (Number num : list) {
            System.out.println(num); // 只能读,安全
        }
        // list.add(10); // 编译报错,不能添加元素
        list.add(null); // 唯一例外,null可以添加
    }
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<Float> floatList = Arrays.asList(1.1f, 2.2f);
        printNumbers(intList); // 正确
        printNumbers(floatList); // 正确
    }
}

2. 下界通配符:? super T(只能写,不能读)

表示 “只能是 T 或 T 的父类”,比如 <? super Integer> 可以接收 IntegerNumberObject 类型。

因为确定集合里的元素是 T 的父类,添加 T 的子类元素肯定安全;但获取元素时,不知道具体是哪个父类,只能用 Object 接收,实用性不高。

public class SuperTest {
    // 接收Integer及其父类的集合
    public static void addIntegers(List<? super Integer> list) {
        list.add(10); // 可以添加Integer类型
        list.add(20);
        // Integer num = list.get(0); // 编译报错,只能用Object接收
        Object obj = list.get(0); // 只能读为Object
    }
    public static void main(String[] args) {
        List<Number> numList = new ArrayList<>();
        List<Object> objList = new ArrayList<>();
        addIntegers(numList); // 正确
        addIntegers(objList); // 正确
        System.out.println(numList); // 输出:[10, 20]
    }
}

3. 无限定通配符:?(不确定类型)

表示 “任意类型”,相当于 <? extends Object>,但比 List<Object> 更灵活(List<String> 可以赋值给 List<?>,但不能赋值给 List<Object>)。

只能读为 Object,不能添加任何元素(除了 null),适合只需要遍历集合、不关心具体类型的场景。

public static void printAny(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
    // list.add("test"); // 编译报错
}

4. PECS 原则

  • Producer Extends:如果集合是 “生产者”(只从中获取元素),用 ? extends T
  • Consumer Super:如果集合是 “消费者”(只向其中添加元素),用 ? super T

比如 Collections.copy(dest, src) 方法,dest 是消费者(接收元素),用 ? super Tsrc 是生产者(提供元素),用 ? extends T,完美契合 PECS 原则。理解这个原则,是掌握Java集合框架中高级用法的关键。

四、类型擦除

泛型看起来强大,但有个隐藏特性:类型擦除。意思是泛型信息只在编译期存在,编译后会被擦除,运行时不存在泛型信息。比如 List<String>List<Integer> 在运行时都是 List 类型,这也是为什么泛型不支持 “类型 instanceof 判断”。

public class ErasureTest {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();
        // 运行时都是List类型,输出true
        System.out.println(strList.getClass() == intList.getClass());
    }
}
  • 不能用泛型类型创建数组,比如 new T[10] 会编译报错;
  • 不能用泛型类型作为 catch 的异常类型;
  • 静态成员不能使用泛型类的类型参数(前面提到过)。

掌握泛型和类型擦除的原理,对于理解 JVM 如何加载和执行Java字节码,以及解决一些复杂的泛型边界问题非常有帮助。

总结

泛型是Java中提升代码类型安全性和可重用性的强大工具。从最基础的泛型类和接口,到灵活运用的泛型方法,再到深入理解通配符和PECS原则,每一步都让我们能写出更健壮、更优雅的代码。虽然类型擦除带来了一些限制,但理解其背后的原理,能帮助我们在基础与综合层面更好地驾驭这门语言。实践这些知识,你的Java代码将告别恼人的 ClassCastException,变得更加可靠和易于维护。如果想深入探讨更多关于泛型的高级主题,或与其他开发者交流心得,云栈社区 是一个不错的去处。




上一篇:品牌如何在AI时代胜出?解读GEO优化与决策入口争夺战
下一篇:华为2026年新机计划曝光:16:10阔屏直板机设计引发热议
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 14:20 , Processed in 0.706830 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表