Day25 | 为什么说Java的泛型是伪泛型

50 阅读6分钟

在之前的Day23 | Java泛型详解Day24 | Java泛型通配符与边界解析两篇文章中,我们学习了泛型的使用方法、通配符以及PECS原则。

大概的理解了泛型在编译期给我们在实际的开发过程中带来的类型安全保证。

今天,我们将深入到泛型的JVM实现原理——类型擦除。

理解了它,你就会明白为什么有人说"Java的泛型是伪泛型"。

一、什么是类型擦除

Java泛型有一个很重要的特点:它只在编译阶段有效。

在.java源文件被编译成.class字节码文件的过程中,泛型信息就会被擦除掉。

类型擦除指的是,在编译后,所有的泛型类型参数都会被替换成它们的边界类型(如果指定了边界)或者Object(如果没有指定边界)。

同时,编译器会在必要的地方自动插入强制类型转换的代码。

换句话说,对于JVM来说,它根本不知道List<String>和List<Integer>的区别,在它眼里,它们都是同一个东西——List。

这是我们写的代码:

package com.lazy.snail.day25;

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

/**
 * @ClassName Day25Demo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/20 14:26
 * @Version 1.0
 */
public class Day25Demo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        Integer i = list.get(0);
        System.out.println(i);
    }
}

JVM看到的代码:

package com.lazy.snail.day25;

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

/**
 * @ClassName Day25Demo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/20 14:26
 * @Version 1.0
 */
public class Day25Demo {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add(1);
        list.add(2);
        Integer i = (Integer) list.get(0);
        System.out.println(i);
    }
}

泛型被清理掉了,而且编译器自动插入了强制类型转换。

这就是为什么我们说Java泛型是编译时的"语法糖",它通过编译器的一些操作。看起来我们可以写出更安全、简洁的代码,但实际上并没有改变JVM的底层运行机制。

那Java官方为什么要这么搞,因为Java在1.5版本才引入泛型。

它为了让之前没有泛型的旧代码和类库(它们处理的都是原始的List、Map等)能在新版JVM上继续运行,并与新的泛型代码无缝协作,才选择了类型擦除这种实现。

它保证了泛型化后的ArrayList<String>在运行时还是那个我们熟悉的ArrayList,解决兼容性问题。

你可能会问,为什么在Java的设计之初没有设计真泛型?

我感觉一个是设计目标,一个是需求的影响。

Java1.0的时候设计目标就是简单、安全、平台无关,没强调泛型的抽象,也没那么强烈的泛型需求。

当然你也可以说这是设计缺陷。

你可能还会问,为什么感知到"缺陷",在JDK1.5的时候引入了泛型,这之前为什么不直接设计真泛型?

我理解是"船大难掉头",没泛型的Java已经广泛应用。

真设计了真泛型,之前的代码都得改,不然升级不了JDK,用不上新的JVM。

引入真泛型要修改JVM规范、字节码格式、反射、运行库等等,本身工程量也不小。

所以就只能"妥协",引入了一直被很多人诟病的"伪泛型"。

二、类型擦除带来的影响与限制

知道了类型擦除这个事后,开发中很多看着奇怪的事情其实就迎刃而解了。

2.1 不能获取泛型类型信息

在运行时,List<String>和List<Integer>是完全相同的类型。

package com.lazy.snail.day25;

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

/**
 * @ClassName Day25Demo2
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/20 15:56
 * @Version 1.0
 */
public class Day25Demo2 {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();
        System.out.println(stringList.getClass() == integerList.getClass());
    }
}

输出结果是true。

这是因为stringList和integerList在运行时的类型都是ArrayList,所以你没办法在运行时通过getClass()方法区分它们。

当然也有例外的情况,通过反射,在某些特定场景下可以获取到泛型信息。

比如,可以获取到方法的参数、返回值或者父类的泛型类型。但这是有条件的,并且比较复杂,我们还没讲到反射,这里就不展开了。

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

同样是因为运行时类型信息被擦除,instanceof关键字也没办法作用在具体的泛型类型上。

package com.lazy.snail.day25;

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

/**
 * @ClassName Day24Demo3
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/20 15:59
 * @Version 1.0
 */
public class Day24Demo3 {
    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();

        // if (integerList instanceof List<Integer>) { ... }
        
        if (integerList instanceof List) {
            System.out.println("It's a List.");
        }
    }
}

编译器直接就不让你写这种代码instanceof List<Integer>,因为它知道这在运行时根本没意义。

2.3 不能创建泛型数组 (new T[])

你不能直接创建一个泛型类型的数组。

package com.lazy.snail.day25;

/**
 * @ClassName Day25Demo4
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/20 16:02
 * @Version 1.0
 */
public class Day25Demo4<T> {
    private T[] data;

    public Day25Demo4(int size) {
        this.data = new T[size];
    }
}

数组(Array)和泛型集合(List<T>)有一个关键区别:数组在运行时必须知道他元素的具体类型。

当你创建一个String[]时,JVM会记录下这个数组的类型是String。

如果你想往里面放一个Integer,JVM会马上就会抛出ArrayStoreException。

而泛型在编译时类型就被擦除了。

如果new T[size]被允许,那么在运行时JVM只知道要创建一个数组,但又不知道T是什么类型。

JVM没办法保证这个数组的类型安全,所以Java干脆在编译层面就不让你这么写代码。

2.4 不能创建泛型异常类

你不能定义一个继承自Throwable(或其子类)的泛型类。

比如这样子:

// public class MyGenericException<T> extends Exception { ... }

这是因为Java的异常处理机制是基于运行时的。

在catch块里,JVM需要确切地知道要捕获的异常类型。

如果异常类是泛型的,类型擦除会导致JVM在catch的时候没办法知道具体的异常类型,那catch机制就失效了。

如果对类型擦除感兴趣,可以去看下源码中com.sun.tools.javac.code.Types和com.sun.tools.javac.comp.TransTypes,这两个是编译器中关于泛型擦除规则和执行的核心类。

结语

今天,我们简单的讲了一下类型擦除。

通过这三天关于泛型的学习,我想大家对下面这些问题都应该有一些简单的认识了。

什么是泛型?

泛型怎么使用?

底层原理是什么?

为什么在使用上有那么多限制?

虽然Java的泛型被称为"伪泛型",还存在一些局限性,但类型擦除是Java在历史发展中为了向后兼容而做出的权衡。

在绝大多数场景下,Java泛型在编译期提供的类型检查能力,已经足够我们写出安全、高质的代码。

下一篇预告

Day26 | Java集合框架概览

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

更多文章请关注我的公众号《懒惰蜗牛工坊》