那些年我们在Java泛型上躺过的枪---万恶的泛型擦除【享学Java】(上)

简介: 那些年我们在Java泛型上躺过的枪---万恶的泛型擦除【享学Java】(上)

前言


泛型(Generics),从字面的意思理解就是泛化的类型,即参数化类型。

我们都知道,泛型是JDK5提供的一个非常重要的新特性,它有非常多优秀的品质:能够把很多问题从运行期提前到编译器,从而使得程序更加的健壮。


但是因为Java5要保持良好的向下兼容性,所以从推出之际一直到现在,它都是个假东西:只在编译期存在,编译成.class文件后就不存在了,这就是所谓的泛型擦除。


C++里的泛型是真实的,它通过类模版的概念去实现。


初识泛型

泛型(generics),从字面的意思理解就是泛化的类型,即参数化类型。


请注意参数化类型和方法参数类型的区别~


泛型类

对比下面两个类,一个是普通类,一个是泛型类:

class Generics {
    Object k;
    Object v;
    public Generics(Object k, Object v) {
        this.k = k;
        this.v = v;
    }
}
class Generics<K, V> {
    K k;
    V v;
    public Generics(K k, V v) {
        this.k = k;
        this.v = v;
    }
}


泛型类的声明一般放在类名之后,可以有多个泛型参数,用尖括号括起来形成类型参数列表。


泛型接口
public interface Generator<T> {
    public T next();
}


这种泛型接口设计,是声明的是一个工厂设计模式常用的生成器接口。比如我们常见的迭代器接口Iterable就是这样一个接口


public interface Iterable<T> {
    Iterator<T> iterator();
}


泛型方法


分为实例泛型方法和静态泛型方法


    public <T> T genericMethod(T t){
        return t;
    }
  // 注意静态方法<T>必须是在static的后面~
    public static <T> T genericStaticMethod(T t){
        return t;
    }


这里需要稍微注意一下:


public class Main<T> {
    // 静态方法不能直接使用类的泛型参数T  而需要自己声明的
    // 形如这样书写才正确:public static <T> T genericStaticMethod(T t) { ... }
    public static T genericStaticMethod(T t) {
        return t;
    }
    // 实例方法可以直接使用类声明的泛型参数
    public T genericMethod(T t) {
        return t;
    }
}


静态方法使用泛型参数需要自己单独声明,否则编译报错。


泛型方法的声明和泛型类的声明略有不同,它是在返回类型之前用尖括号列出类型参数列表(也可以有多个泛型类型),而函数传入的形参类型可以利用泛型来表示。


泛型的局限性

总结出如下几种情况,使用泛型的时候务必注意:


  1. 基本类型无法作为类型参数。如ArrayList<int>这样是非法的,而只能ArrayList<Integer>1. 请注意:数组表示中int[]和Integer[]都是可以的
  2. 在泛型代码内部,无法获得任何有关泛型参数类型的信息。比如你传入的泛型参数为T,而在方法内部你无法使用T的任何方法,毕竟编译期它的类型还不确定1. 对此句话我做个备注:并不是什么方法都不能使。如果你的泛型<T extends BaseDBEntity>的话,你是可以使用BaseDBEntity方法的,但如果你没有任何继承,那你只能使用Object的方法(毕竟任何类型都是它的子类嘛~)
  3. 在能够使用泛型方法的时候,尽量避免使整个类泛化。粗细粒度需要控制好~1. 当你大部分方法都用了泛型的时候,建议将类泛化


泛型擦除


前面指出了,Java的泛型是假的,它是编译期的。下面通过一个示例证明这一点:


public class Main {
    public static void main(String[] args) {
        List<String> c1 = new ArrayList<>();
        List<Integer> c2 = new ArrayList<>();
        Class<? extends List> class1 = c1.getClass();
        Class<? extends List> class2 = c2.getClass();
        System.out.println(class1 == class2);
    }
}

输出结果为:true

其实这里可能有不少小伙伴不能理解了,泛型中明明给它传的类型是不一样的,为何Class还是一样的呢???这里就要说到Java语言实现泛型所独有的——泛型擦除。


本例说明了:当我们声明List<String>和List<Integer>时,在运行时实际上是相同的,都是List,而具体的类型参数信息String和Integer被擦除了。(具体有兴趣的小伙伴可以去查看它的.class文件内容(可反编译后查看对比))


由上可知,下面结果是:false


    public static void main(String[] args) {
        List<String> c1 = new ArrayList<>();
        List<Integer> c2 = new LinkedList<>();
        Class<? extends List> class1 = c1.getClass();
        Class<? extends List> class2 = c2.getClass();
    // 一个是ArrayList  一个是LinkedList 所以返回false
        System.out.println(class1 == class2); // false
    }


为什么Java要擦除泛型?


从上例可以知道,java的泛型擦除确实给实际编程中带来了一些不便(特别是运行时反射时,有时候无法判断出真实类型)。那Java的设计者为什么要这么干呢?


这是一个历史问题,Java在版本1.0(1.5之前)中是不支持泛型的,这就导致了很大一批原有类库是在不支持泛型的Java版本上创建的。而到后来Java逐渐加入了泛型,为了使得原有的非泛化类库能够在泛化的客户端使用,Java开发者使用了擦除进行了折中(保持向下兼容)。


所以Java使用这么具有局限性的泛型实现方法就是从非泛化代码到泛化代码的一个过渡,以及不破坏原有类库的情况下,将泛型融入Java语言。


Java泛型擦除是擦除所有吗?


泛型擦除其实是分情况擦除的,不是完全擦除,一定要消除这个误区。

Java 在编译时会在字节码里指令集之外的地方保留部分泛型信息,泛型接口、类、方法定义上的所有泛型、成员变量声明处的泛型都会被保留类型信息,其他地方的泛型信息都会被擦除。


这就是为什么上面这些地方的泛型,你在运行期还是可以通过反射获取的原因(如果是真的全部擦除了,运行期就找不到了)

当然喽,你也可以理解是全部擦除了–> 只是找了另外一个地方存放而已~


泛型通配符


<? super T><? extends T><?>

关于通配符的使用,本文略~



泛型使用示例(坑)


使用集合框架时请务必明确泛型


如下使用案例,存在非常大的风险

public static void main(String[] args) {
    List list = new ArrayList();
    list.add("1");
    list.add(2);
    List<String> list2 = list; //不报错 因为类型一样都是List类型
    System.out.println(list2); //[1, 2] 这样是不抱错的哦
    //报错 类型转换异常   这时候里面的元素一碰就报错
    list2.forEach(x -> System.out.println(x));
}


如上由于第一个List没有加上泛型,使得它里面既装了String,又装了Integer,所以最后只要一get出来就报错了(迭代也不行)。


这个也是通过反射完成一些封装的框架,比如MyBatis、Redis序列化值、SpringMVC等处理入参的值普遍会遇到但是无法解决的问题


泛型类型的可变参数作为入参的坑


如下示例:

public class Main {
    public static void main(String[] args) {
        Integer[] ints1 = new Integer[]{1, 2, 3};
        int[] ints2 = new int[]{1, 2, 3};
        // 注意下面两种输出结果是不一样的
        doSomething(ints1); //输出1,2,3
        doSomething(ints2); //输出[I@1f32e575
    }
    // 静态泛型方法  需要自己申明泛型T
    // 静态泛型方法  需要自己申明泛型T
    private static <T> void doSomething(T... values) {
        //boolean b = values instanceof Object; // 编译不抱错
        //boolean b = values instanceof List; // 编译报错:不能转换为List类型
        for (T value : values) {
            System.out.println(value);
        }
    }
}


这个结果是由于int[]这个数组最终就被当作一个数值作为入参了。

通过此例可以总结出如下两点:


  1. 泛型的类型参数只能是类类型,不能是简单类型
  2. 不能对不确切的泛型类型使用instanceof操作(如上例子泛型类型若没指定上限,都是Object的子类而已)





相关文章
|
2天前
|
存储 Java 编译器
Java泛型类型擦除以及类型擦除带来的问题
本文主要讲解Java中的泛型擦除机制及其引发的问题与解决方法。泛型擦除是指编译期间,Java会将所有泛型信息替换为原始类型,并用限定类型替代类型变量。通过代码示例展示了泛型擦除后原始类型的保留、反射对泛型的破坏以及多态冲突等问题。同时分析了泛型类型不能是基本数据类型、静态方法中无法使用泛型参数等限制,并探讨了解决方案。这些内容对于理解Java泛型的工作原理和避免相关问题具有重要意义。
|
4月前
|
存储 Java 编译器
Java泛型类型擦除以及类型擦除带来的问题
泛型擦除是指Java编译器在编译期间会移除所有泛型信息,使所有泛型类型在运行时都变为原始类型。例如,`List&lt;String&gt;` 和 `List&lt;Integer&gt;` 在JVM中都视为 `List`。因此,通过 `getClass()` 比较两个不同泛型类型的 `ArrayList` 实例会返回 `true`。此外,通过反射调用 `add` 方法可以向 `ArrayList&lt;Integer&gt;` 中添加字符串,进一步证明了泛型信息在运行时被擦除。
100 2
|
5月前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
140 0
[Java]泛型
|
5月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
47 1
|
5月前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
54 5
|
5月前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
37 1
|
5月前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
38 2
|
5月前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
49 0
|
26天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
157 60
【Java并发】【线程池】带你从0-1入门线程池
|
15天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
66 23