【Java泛型一遍过】

泛型是 Java 的一大利器,它将类型检查从运行时提前到了编译时。这篇笔记不讲废话,只记录核心概念、易错点和个人的一些思考。

1. 为什么需要泛型?------ 类型安全

在没有泛型的时代,我们用 Object 来容纳任意类型的数据,但这带来了两个问题:

  1. 需要强制类型转换:取出来时,必须手动强转,容易出错。
  2. 类型不安全 :编译器无法检查你放的类型是否正确,只在运行时抛出 ClassCastException
java 复制代码
// 旧时代
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译通过,但逻辑上可能出错
// 取出时必须强转,且容易出错
String str = (String) list.get(0); // 正常
Integer num = (Integer) list.get(1); // 正常
// String error = (String) list.get(1); // 运行时抛出 ClassCastException!

泛型的出现,就是为了解决这个问题。它像一个标签,告诉编译器这个容器里应该放什么类型的东西。

java 复制代码
// 泛型时代
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译时直接报错!完美!
String str = list.get(0); // 无需强转,安全、简洁

*由此泛型将类型安全的责任,转移给了编译器自动检查。

2. 泛型的基本使用

泛型可以用在类、接口和方法上。

  • 泛型类/接口<T> 是一个类型占位符,可以是任意字母(T, E, K, V 等)。

    java 复制代码
    // 泛型类
    public class Box<T> {
        private T content;
        public void setContent(T content) {
            this.content = content;
        }
        public T getContent() {
            return content;
        }
    }
    // 使用
    Box<String> stringBox = new Box<>();
    stringBox.setContent("Hello World");
  • 泛型方法 :方法拥有自己的类型参数,独立于类。

    java 复制代码
    public class Util {
        // <T> 是声明这是一个泛型方法,T 是方法的类型参数
        public static <T> T getValue(T[] array, int index) {
            return array[index];
        }
    }
    // 使用,编译器会自动推断 T 的类型
    String value = Util.getValue(new String[]{"a", "b", "c"}, 1);
    Integer num = Util.getValue(new Integer[]{1, 2, 3}, 2);

思考:泛型方法什么时候用?当一个方法需要处理的逻辑是通用的,但操作的类型不确定时,泛型方法就非常合适。它比泛型类更灵活,因为它的类型作用域仅限于方法内部。

3. 进阶理解:通配符与 PECS 原则

这是泛型最容易混淆的地方,但也是精髓所在。

假设我们有这样的继承关系:NumberInteger 的父类。

java 复制代码
List<Integer> integerList = new ArrayList<>();
// List<Number> numberList = integerList; // 编译报错!

报错是因为只是容器类的元素有继承关系而他们整体 List<Integer>List<Number> 之间没有继承关系。List<Integer> 不是 List<Number> 的子类型。如果允许这样赋值,就会破坏类型安全:

java 复制代码
// 假设上面那行代码能编译通过
List<Number> numberList = integerList; // integerList 里只能放 Integer
numberList.add(3.14); // 如果能通过,这里就放入了 Double
Integer i = integerList.get(0); // 取出时就会得到 Double,强转失败!

为了解决这种"泛型继承"的问题,Java 引入了通配符 ?

3.1 上界通配符 ? extends T (Producer Extends)

List<? extends Number> 表示一个可以存放 Number 或其子类型(Integer, Double...)的列表,但我们不知道具体是哪种。

  • 特点 :只能读取 ,不能写入 (除了 null)。
  • 适用场景 :作为数据生产者,向外提供数据。
java 复制代码
public void sum(List<? extends Number> list) {
    double sum = 0.0;
    for (Number n : list) {
        sum += n.doubleValue(); // 安全,因为 list 里的元素肯定是 Number 或其子类
    }
    System.out.println("Sum: " + sum);
    // list.add(1); // 编译错误!编译器不知道 list 的具体类型,可能是 List<Double>,不能放 Integer
    // list.add(1.0); // 编译错误!同理,可能是 List<Integer>
}
3.2 下界通配符 ? super T (Consumer Super)

List<? super Integer> 表示一个可以存放 Integer 或其父类型(Number, Object...)的列表。

  • 特点 :只能写入 T 及其子类型,不能精确读取 (只能读出 Object)。
  • 适用场景 :作为数据消费者,接收数据。
java 复制代码
public void addNumbers(List<? super Integer> list) {
    list.add(1); // 安全,Integer 是 Integer 的子类
    list.add(2); // 安全
    // Number num = list.get(0); // 编译错误!list 可能是 List<Object>,取出来不一定是 Number
    Object obj = list.get(0); // 只能确定是 Object
}
3.3 PECS 原则:Producer Extends, Consumer Super

这是 Joshua Bloch 在《Effective Java》中提出的著名原则,是判断使用 extends 还是 super 的黄金法则。

  • 如果你只需要从集合中读取数据(生产者),就用 ? extends T
  • 如果你只需要向集合中写入数据(消费者),就用 ? super T
  • 如果既要读又要写,那就不要用通配符,用具体的泛型类型 <T>
4. 底层机制:类型擦除

泛型是 Java 的语法糖,它在编译期有效,但在运行期会被"擦除"。

  • 擦除规则
    • 所有泛型类型参数都会被替换为它们的边界<T extends Number> 擦除后是 Number)。
    • 如果没有边界,则被替换为 Object
    • 因此,在运行时,List<String>List<Integer> 其实都是 List
java 复制代码
// 编译前
List<String> stringList = new ArrayList<>();
stringList.add("abc");
// 编译后(虚拟机看到的代码类似这样)
List stringList = new ArrayList();
stringList.add("abc");
// 在 get() 时,编译器会自动插入一个强转 (String)
String s = (String) stringList.get(0);

类型擦除带来的限制与思考

  1. 不能创建泛型数组T[] array = new T[10]; 是非法的。

    • 原因 :类型擦除后,new T[] 会变成 new Object[]。而 Object[] 不能被强转为 String[] 等,会抛出 ArrayStoreException。这会破坏数组原本的类型安全机制。
    • 解决方案
      • 使用 ArrayList<T> 代替数组,这是首选。
      • 使用反射 Array.newInstance(Class<?> componentType, int length),并传入 Class 对象。
  2. instanceof 操作符不能用于泛型类型obj instanceof List<String> 是非法的。

    • 原因 :运行时 List<String> 就是 List,无法区分。
    • 解决方案 :使用通配符 obj instanceof List<?>
  3. 静态方法/变量不能使用类的泛型参数

    • 原因 :泛型是实例级别的,每个实例的 T 可能不同。而静态成员是类级别的,属于所有实例共享。编译器无法确定静态成员应该使用哪个 T
    • 解决方案 :如果静态方法需要泛型,必须声明为独立的泛型方法 ,即自己拥有 <T>
    java 复制代码
    public class MyClass<T> {
        // private static T data; // 编译错误
        public static <U> U staticGenericMethod(U input) { // <U> 声明这是个泛型方法
            return input;
        }
    }
5. 实践技巧:使用 Class<T> 传递类型信息

由于类型擦除,我们在运行时无法获取 T 的具体类型。一个常见的变通方法是传入 Class<T> 对象。

java 复制代码
public class JsonParser {
    // Gson/Jackson 等库都大量使用这种模式
    public static <T> T fromJson(String json, Class<T> clazz) {
        // ... 解析逻辑
        // 通过 clazz.newInstance() 或其他方式创建实例
        // 通过反射获取字段类型
        return null; // 示例
    }
}
// 使用
User user = JsonParser.fromJson("{\"name\":\"test\"}", User.class);

思考Class<T> 就像是运行时泛型 。它把编译时的类型信息延续到了运行时,弥补了类型擦除的缺陷。

总结
  • 核心价值 :泛型是编译时的类型安全工具。
  • 关键语法:掌握泛型类、泛型方法的定义。
  • 精髓 :理解 PECS 原则,这是灵活使用通配符的关键。
  • 底层认知 :理解类型擦除,能解释很多"为什么不行"的问题,并找到变通方案。
  • 实践 :遇到类型擦除的坑时,考虑使用 Class<T> 对象作为类型令牌。

相关推荐
BD_Marathon38 分钟前
【JavaWeb】JS_数据类型和变量
开发语言·javascript·ecmascript
AI浩1 小时前
【Redis】Windows下Redis环境搭建与使用详细教程
数据库·windows·redis
卿雪1 小时前
认识Redis:Redis 是什么?好处?业务场景?和MySQL的区别?
服务器·开发语言·数据库·redis·mysql·缓存·golang
..空空的人1 小时前
C++基于protobuf实现仿RabbitMQ消息队列---接口介绍
开发语言·c++·rabbitmq
骇客野人1 小时前
JAVA获取一个LIST中的最大值
java·linux·list
JIngJaneIL1 小时前
基于Java失物招领系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·vue
程序员岳焱1 小时前
Java泛型高级玩法:通配符、上下界与类型擦除避坑实战(纯干货,附完整工具类)
java·后端·程序员
期待のcode1 小时前
MyBatis-Plus基本CRUD
java·spring boot·后端·mybatis
❀͜͡傀儡师1 小时前
maven 仓库的Central Portal Namespaces 怎么验证
java·maven·nexus