详细探讨:为什么 Java 不支持泛型数组?

引言

        泛型根据字面意思就是,广泛的类型。它可以表示任意一个具体的类型,这就相当于一种表示类型的变量。例如在声明一个一个变量a的时候,如果我们想让这个a根据业务逻辑的不同,可以有多种表现,也就是可以有不同的数据类型的时候,就可以用到泛型。用泛型声明这个变量a。

        int a; // 引入泛型前的写法:定死了,只能是int类型
        T a; // 引入后 ,类型就是动态的,可以根据业务需求让a声明为对应的类型

泛型是如何实现的?

      试想一下,如果让你来实现,你会怎么实现泛型?难点就在于如何能让类型实现这种动态绑定,如何根据传入的类型来决定某个参数的类型。

这时可以想到Object,因为它是所有类的超类,如果我用Object来声明变量,那我可以将任意类型对象赋值给这个变量(向上转型)。取变量值的时候只需要向下转型即可。

class Zoo{
    Object animal;
    public Zoo(Object animal){
        this.animal = animal;
    }
}
class Cat{
    public void eat(){
        System.out.println("猫吃鱼");    
    }
}

class Dog{
     public void eat(){
        System.out.println("狗吃骨头");    
    }
}
public class Test{
    public static void main(String[] args){
        Zoo zoo1= new Zoo(new Cat()); // 创建一个有小猫的动物园
        Cat cat = (Cat)zoo1.animal; // 输出猫对象
        cat.eat(); // 输出 猫吃鱼
        Zoo zoo2= new Zoo(new Dog()); // 创建一个有小狗的动物园
        Dog dog = (Dog)zoo2.animal; // 输出狗对象
        dog.eat(); // 输出 狗吃骨头
    }
}

         上述代码就是在不引用泛型的情况下,设计Zoo动物园类,可以接收任意类型的动物。因为不知道属性animal的具体类型(可以是猫也可以是狗),所以将属性animal设为Object类型,这样无论什么类型都可以通过类型自动向上提升为Object从而被放进animal中。虽然我们想要的效果实现了,但是它的代码却显得很不智能和繁琐,因为在取出的时候还需强转为你放入的类型,而强转的操作都是由程序员自己决定和自己承担风险,这也就意味着程序员如果想正确的运行不同动物的eat方法还需要自己检查实际运行类型是否和强转类型一致。

        其实更严谨的设计一点,应该再设计一个Animal类,然后让Cat类和Dog类继承这个类,让Zoo的成员属性animal静态编译类型为Animal,既可以保证接收任意动物,也可以保证接收的一定是动物类型。对于限制类型这一点,泛型的设计者也考虑到了,所以有了泛型边界。如:< ? extends Animal> 限制上界,最高父类为Animal, 限制下界: <? super Animal>最低子类为Animal。

而实际上泛型的设计也确实是通过类似的方式实现的,因为泛型到编译时期就会全部擦除为Object类型,而有上界则会被擦除为上界类型(< ? extends Animal>会被擦除为Animal),有下界的依然被擦除为Object。那既然这样为什么不直接用Object实现这种功能,还要再设计出泛型呢?因为其实泛型还有一个好处就是带来编译时可以进行类型检查,它不允许你把不兼容的数据赋值给你传入的类型修饰的变量。有了这层检查机制之后,也就自然不需要你再强转了,因为编译器认为他已经检查了你数据的类型是一致的。

例如你声明了List<String> list;那么这个list只会允许你加入String类型的数据,你加入其他类型数据,编译器会直接就报红,不会到运行的时候才抛出异常,直接就在编写代码的时候把问题暴露给你,让你进行处理。所以这也是为什么可以把泛型擦除为Object,因为在编译层面它就保证了你加入的只能是你传入的类型(保证了类型安全),这个时候你的编译类型被擦除成什么都不重要了。

为什么不能创建泛型实例或泛型数组?

如果你了解了泛型的本质以及是如何实现泛型的,就可以以此为基础去看待这个问题。前文说了泛型在编译时期就会被全部被擦除为Object(没规定上界的情况下),也就是说泛型这个机制其实只是存在在编写代码时期,也就是只存在你的.java文件中,并且编译器在这个期间会进行类型检查,仅此而已。

假设可以创建泛型实例:

class A<T>{
    
    T instance = new T(); // 相当于 Object instance = new Object();
}

public class Test{
     public static void main(String[] args) {
        A<Date> a = new A<>();
        Date date = a.instance; // 会运行转换异常,因为实际运行类型是Object
        date.getTime(); 
    }
}

假设传入的类型是一个Date类,明明是想要创建一个Date的实例,而实际创建的却是一个Object对象。从这里就可以看出Java对泛型的设计并没有这么智能,它只停留在静态层面,并不能实现动态的创建一个我们想要的实例对象。这时候可能就会疑问了,既然泛型并不能创建实例对象,那泛型又是怎么让取出的对象确实是传入类型的实例呢?

还是以List为例:

List<String> list = new ArrayList<>();
list.add("hhh");
String s = list.get(0);
System.out.print(s); // 打印出 hhh

这段代码就可以发现,我们想要一个装String类型的list,而String的实例全都是由程序员自己写入创建的,并不是利用泛型在底层帮我们动态实现的,泛型仅仅是在此时检查了add的类型只能为String。其次就是就算真的能动态创建实例对象,但是像T a = new T(); 代码本身也是有问题的,因为万一传入的类型正好是没有无参构造方法的,运行就会报异常。

 至此如果你明白了为什么不能创建泛型实例,相信也会对为什么不能创建泛型数组有一定体会。但是还是有细微区别。因为就算是创建Object[]数组,其实对于数组中存储的每个元素来讲也是没有影响的,只是对数组整体来讲会有影响。因为数组的每个元素依然是程序员自己创建的实例然后放入数组。这样想好像泛型数组没有什么大问题,因为数组中每个元素的实例是程序员自己创建的,而泛型的编译检查机制又会限制程序员只能在数组中放入传入类型的实例。看似保证了安全,其实还是会有隐藏的安全问题。

假设可以创建泛型数组:

class A<T>{
    T[] arr = new T[]; // 擦除后: Object[] arr = new Object[];
}

class Test{
    public static void main(String[] args) {
        A<Integer> a = new A<>();
        a.arr[0] = 1; // 成功放入
        a.arr[1] = "hhhh"; // 编译报错,编译器检查出了和传入的类型不匹配
        Object[] obj = a.arr; 
        obj[1] = "hhh"; // 成功放入
        Intger[] arr = a.arr; // 类型转换异常 ---->  Object数组不能转换为Integer数组
    }
    
}

        从上面的代码就可以看出,创建泛型数组不仅可能引发像创建泛型实例那样的类型转换异常,还有可能绕过编译检查机制,放入和传入类型不兼容的数据类型。因为数据具有协变性(协变性指:子父类数组的引用可以指向子类数组),所以Object[] obj = a.arr; 这行代码当然是可行的,故obj[1] = "hhh"; 也是可以成功执行的,因为编译器检查发现“hhh”为String类型,obj的静态类型为Object,而String是Object的子类,当然可以允许放入。这样就绕过了检查,并且也不会有运行异常,因为数组的实际运行类型就是Object数组。Object数组的数组元素可以是任意对象。这样就违背了我们的本意,我们传入类型参数Integer就是想让数组只存放Integer类型的数据,但是编译器的检查机制被绕过了,并且运行时也不会报异常,那就会存在一种问题,就是数组里面可能已经含有不是Integer的数据了,但是却无法被发现,这种错误可能会导致后续处理一些代码逻辑时造成安全问题。

        并且从数组的层面上,数组的协变性是基于数组的类型安全检查的,也就是数组能够记住元素的类型并进行运行期类型检查。

Number[] numbers=new Integer[10];
numbers[0]=new Double(3.14); // 报错 类型转换异常

上述代码就体现了数组的安全性,因为问题是可以被暴露出来的。泛型数组违背了这种安全机制。

那么就不能使用泛型数组了吗?其实Java只是不允许创建泛型数组,但是可以使用泛型数组。如下:

class A<T>{
    T[] arr =(T[]) new Object[]; 
}

其实好像跟之前的写法没有区别,并且在擦除之后,确实也是跟之前的写法一样的。那为什么这样写就允许呢?因为此时数组是new的一个具体类型,然后再进行强转,这意味着编译器已经提醒你有风险了,但是你依然决定强转,所以你就得自己承担这个风险。

扩展:正确创建泛型数组的方式

可以利用反射来创建泛型数组,这其实相当于真的创建了一个传入类型的实例数组,而不是Object数组。

还是根据上面的测试代码,测试这样创建泛型数组会有什么不一样的结果。

class A<T>{
    T[] arr;
    public A(Class<T> clazz){
        arr = (T[])Array.newinstance(clazz,10);
    }
}
class Test{
    public static void main(String[] args) {
        A<Integer> a = new A<>();
        a.arr[0] = 1; // 成功放入
        a.arr[1] = "hhhh"; // 编译报错,编译器检查出了和传入的类型不匹配
        Object[] obj = a.arr; 
        obj[1] = "hhh"; // 类型转换异常 ---> String类型不能转换为Integer
        Intger[] arr = a.arr; // 成功取出Integer数组
    }
    
}

根据测试结果,可以得出这样创建泛型数组无论在什么层面来看都是安全有保证的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/955837.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

单片机存储器和C程序编译过程

1、 单片机存储器 只读存储器不是并列关系&#xff0c;是从ROM发展到FLASH的过程 RAM ROM 随机存储器 只读存储器 CPU直接存储和访问 只读可访问不可写 临时存数据&#xff0c;存的是CPU正在使用的数据 永久存数据&#xff0c;存的是操作系统启动程序或指令 断电易失 …

UDP报文格式

UDP是传输层的一个重要协议&#xff0c;他的特性有面向数据报、无连接、不可靠传输、全双工。 下面是UDP报文格式&#xff1a; 1&#xff0c;报头 UDP的报头长度位8个字节&#xff0c;包含源端口、目的端口、长度和校验和&#xff0c;其中每个属性均为两个字节。报头格式为二…

2024年我的技术成长之路

2024年我的技术成长之路 大家好&#xff0c;我是小寒。又到年底了&#xff0c;一年过得真快啊&#xff01;趁着这次活动的机会&#xff0c;和大家聊聊我这一年在技术上的收获和踩过的坑。 说实话&#xff0c;今年工作特别忙&#xff0c;写博客的时间比去年少了不少。不过还是…

HTML5+Canvas实现的鼠标跟随自定义发光线条源码

源码介绍 HTML5Canvas实现的鼠标跟随自定义发光线条特效源码非常炫酷&#xff0c;在黑色的背景中&#xff0c;鼠标滑过即产生彩色变换的发光线条效果&#xff0c;且线条周围散发出火花飞射四溅的粒子光点特效。 效果预览 源码如下 <!DOCTYPE html PUBLIC "-//W3C//D…

爬虫第二篇

太聪明了怎么办&#xff1f;那就&#xff0c;给脑子灌点水&#xff01;&#xff01; 本篇文章我们来简单讲一下如何爬取mv,也就是歌曲视频&#xff0c;那么我们进入正题。 由于上次拿网易云开了刀&#xff0c;那么这次我们拿酷狗开刀。 还是进入上次讲过的页面 注意&#xff…

C#表达式和运算符

本文我们将学习C#的两个重要知识点&#xff1a;表达式和运算符。本章内容会理论性稍微强些&#xff0c;我们会尽量多举例进行说明。建议大家边阅读边思考&#xff0c;如果还能边实践就更好了。 1. 表达式 说到表达式&#xff0c;大家可能感觉有些陌生&#xff0c;我们先来举个…

Jira中bug的流转流程

Jira中bug的状态 1. 处理Bug的流程2. bug状态流转详述bug的状态通常包括 1. 处理Bug的流程 2. bug状态流转详述 bug的状态通常包括 未解决 1. 测试人员创建一个bug&#xff0c;填写bug的详细信息&#xff0c;如概要、bug级别、复现步骤、现状、预期结果等 2. 定位bug&#x…

快手极速版如何查找ip归属地?怎么关掉

在数字化时代&#xff0c;个人隐私的保护成为了广大用户关注的焦点。快手极速版作为一款备受欢迎的短视频应用&#xff0c;其IP归属地的显示与关闭功能自然也成了用户热议的话题。本文将详细介绍如何在快手极速版中查找IP归属地以及如何关闭IP属地显示&#xff0c;帮助用户更好…

BGP边界网关协议(Border Gateway Protocol)路由引入、路由反射器

一、路由引入背景 BGP协议本身不发现路由&#xff0c;因此需要将其他协议路由&#xff08;如IGP路由等&#xff09;引入到BGP路由表中&#xff0c;从而将这些路由在AS之内和AS之间传播。 BGP协议支持通过以下两种方式引入路由&#xff1a; Import方式&#xff1a;按协议类型将…

Solidity03 Solidity变量简述

文章目录 一、变量简述1.1 状态变量1.2 局部变量1.3 全局变量1.4 注意问题 二、变量可见性2.1 public2.2 private2.3 internal2.4 默认可见性2.5 可见性的用处 三、变量初始值3.1 值类型初始值 一、变量简述 变量是指可以保存数据的内部存储单元&#xff0c;里面的数据可以在程…

数据结构---并查集

目录 一、并查集的概念 二、并查集的实现 三、并查集的应用 一、并查集的概念 在一些实际问题中&#xff0c;需要将n个不同的元素划分成一些不相交的集合。开始时&#xff0c;每个元素自成一个单元素集合&#xff0c;然后按一定的规律将归于同一组元素的集合…

STM32 FreeRTOS内存管理简介

在使用 FreeRTOS 创建任务、队列、信号量等对象时&#xff0c;通常都有动态创建和静态创建的方式。动态方式提供了更灵活的内存管理&#xff0c;而静态方式则更注重内存的静态分配和控制。 如果是1的&#xff0c;那么标准 C 库 malloc() 和 free() 函数有时可用于此目的&#…

构建core模块

文章目录 1.环境搭建1.sunrays-common下新建core模块2.引入依赖&#xff0c;并设置打包常规配置 2.测试使用1.启动&#xff01;1.创建模块2.引入依赖3.application.yml 配置MySQL和Minio4.创建启动类5.启动测试 2.common-web-starter1.目录2.WebController.java3.结果 3.common…

【Flink系列】6. Flink中的时间和窗口

6. Flink中的时间和窗口 在批处理统计中&#xff0c;我们可以等待一批数据都到齐后&#xff0c;统一处理。但是在实时处理统计中&#xff0c;我们是来一条就得处理一条&#xff0c;那么我们怎么统计最近一段时间内的数据呢&#xff1f;引入“窗口”。 所谓的“窗口”&#xff…

AIGC与劳动力市场:技术进步与就业结构的重塑

随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;尤其是生成式AI&#xff08;AIGC&#xff09;&#xff0c;劳动力市场正经历前所未有的变革。从内容创作到自动化生产线&#xff0c;几乎每个行业都在经历一场技术的洗礼。然而&#xff0c;这场革命并不是全然…

废品回收小程序,数字化回收时代

随着科技的不断创新发展&#xff0c;废品回收在各种技术的支持下也在不断地创新&#xff0c;提高了市场的发展速度&#xff0c;不仅能够让回收效率更加高效&#xff0c;还能够让居民更加便捷地进行回收&#xff0c;推动废品回收行业的发展。 回收市场机遇 目前&#xff0c;废…

题解 CodeForces 430B Balls Game 栈 C/C++

题目传送门&#xff1a; Problem - B - Codeforceshttps://mirror.codeforces.com/contest/430/problem/B翻译&#xff1a; Iahub正在为国际信息学奥林匹克竞赛&#xff08;IOI&#xff09;做准备。有什么比玩一个类似祖玛的游戏更好的训练方法呢&#xff1f; 一排中有n个球…

【Linux】线程全解:概念、操作、互斥与同步机制、线程池实现

&#x1f3ac; 个人主页&#xff1a;谁在夜里看海. &#x1f4d6; 个人专栏&#xff1a;《C系列》《Linux系列》《算法系列》 ⛰️ 道阻且长&#xff0c;行则将至 目录 &#x1f4da;一、线程概念 &#x1f4d6; 回顾进程 &#x1f4d6; 引入线程 &#x1f4d6; 总结 &a…

PDF文件提取开源工具调研总结

概述 PDF是一种日常工作中广泛使用的跨平台文档格式&#xff0c;常常包含丰富的内容&#xff1a;包括文本、图表、表格、公式、图像。在现代信息处理工作流中发挥了重要的作用&#xff0c;尤其是RAG项目中&#xff0c;通过将非结构化数据转化为结构化和可访问的信息&#xff0…

简历_使用优化的Redis自增ID策略生成分布式环境下全局唯一ID,用于用户上传数据的命名以及多种ID的生成

系列博客目录 文章目录 系列博客目录WhyRedis自增ID策略 Why 我们需要设置全局唯一ID。原因&#xff1a;当用户抢购时&#xff0c;就会生成订单并保存到tb_voucher_order这张表中&#xff0c;而订单表如果使用数据库自增ID就存在一些问题。 问题&#xff1a;id的规律性太明显、…