Java泛型

使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。

一、为什么要使用泛型程序设计

1、增加泛型类之前

Java中增加泛型类之前,泛型程序设计是用继承实现的。

    比如下面的代码中,ArrayList类只维护一个Object引用的数组:

    这样的实现有两个问题:

        ①当获取一个值时必须进行强制类型转换。

        ②这里没有错误检查,可以向数组列表中添加任何类的对象。

没有错误检查,可能导致的问题:如上所示,将一个File对象放进了容器中(编译和运行都不会出错),之后取出来强转成String类型,就会报错。

2、增加泛型类之后

二、定义简单泛型类

    要知道泛型类的本质,使用泛型类就是为了编写的代码可以被很多不同类型的对象所复用,例如我们并不希望为聚集(容器)StringFile对象分别设计不同的类,所以设计一个泛型类ArrayList<T>,泛型类就相当于一个模板类,可以适用于很多类(不要被这个T束缚,首先要明白泛型类是一个类,这里我们想要一个容器类,只是想让这个容器可以装很多不同的类的对象,所以就用T来代表被装的不同的类,泛型类最关键的是要明白它本身要干什么T只是一个参数;还有一点,这个ArrayList里面保持了一个数组或者啥的也是一个迷惑点,泛型类不是代表它要装多少个对象,而是取决于它要干什么;比如我们可以设计一个泛型类Add<T>,它是想要将两个对象相加,那么T可以是不同类型的。。。)

    一个泛型类就是具有一个或多个类型变量的类,比如下面的代码:

    用具体的类型替换类型变量后,可以将得到的Pair<String>想象成一个普通类:

具有构造器:

具有方法:

        换句话说,泛型类可以看作普通类的工厂。

三、泛型方法

    前面已经介绍了如何定义一个泛型类。实际上,还可以定义一个带有类型参数的简单方法:

    这个方法是在普通类中定义的,然而,这是一个泛型方法(可以从尖括号和类型变量看出这一点)。注意,类型变量是放在修饰符(public static)的后面,返回类型的前面。

    泛型方法可以定义在普通类中,也可以定义在泛型类中。

四、类型变量的限定

    有时,类或方法需要对类型变量加以约束限定(就是规定类型变量只能是哪些类)。

    下面的例子,要找出数组中的最小元素:

五、泛型代码和虚拟机

    虚拟机中没有泛型类型对象,即所有对象都属于普通类。

    无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删除类型参数名和尖括号后的泛型类型名。将泛型类型的代码变成原始类型的代码:其它保持不变,改变类名,将类型参数(比如T)做修改,如果T有限定类型,就将T换成T第一个限定的类型变量;如果T没有限定类型,就将T换成Object。比如:

Pair<T>的代码:

    Pair<T>的原始类型代码:

    在程序中可以包含不同类型的Pair,例如,Pair<String>Pair<Date>,而擦除类型后就变成原始的Pair类型了。

    另一个关于转换原始类型的例子:

1、翻译泛型表达式

虚拟机中没有泛型类型对象,即所有对象都属于普通类。因为Java程序是运行在Java虚拟机中的,所以运行时会有类型擦除。

注意:在运行时,代码的类型被擦除,但对象的类型不会被擦除。所谓被擦除,是指Java虚拟机将运行的是擦除后生成的一份字节码。

2、翻译泛型方法

注意:运行时的类型擦除是把泛型类的所有代码都擦除成了原始类型,所以只要涉及到泛型代码,思考运行时都是想着把代码擦除,然后只不过编译器会在运行时,将方法调用的代码翻译成虚拟机指令的过程中,又把类型强转回来。

所以在DateInterval类中就存在两个setSecond()方法,一个的类型参数是Date,一个的类型参数是Object。(在运行时,因为擦除,上面的重写实际上变成了重载,而我们想要达到的目的是重写【重写要求子类和父类的方法名相同,方法类型相同,extends Pair<Date>,用Date实例化了T,所以重写时就想的是把父类的setSecond(T second)T也用Date来实例化,但是达不到目的,见下面】)

考虑下面的语句序列:

由于Pair<Date>类型的引用pair指向了一个DateInterval类型的对象(父类引用指向了子类对象),所以pair.setSecond应该调用子类的setSecond方法,按照多态的想法,想调用的应该是子类重写父类的那个方法,pair因为是父类Pair<Date>类型的(这里的子类是DateInterval,子类对象是interval),它要调用的setSecond()只能是父类定义的(多态的基本原理),【我们所想的:setSecond(T a)setSecond(Date a)应该是属于重写的范畴,所以我们认为父类Pair<T>类型的pair指向一个子类对象时,调用setSecond会调用子类对象的setSecond(Date a)】,但是不是这样的,由于类型擦除,运行代码时,Java虚拟机进行擦除,生成一份新的擦除后的代码,运行的就是这份新的代码,父类的setSecond(T a)被擦除成了setSecond(Object a),这时候子类没有重写父类的setSecond(Object a)方法(因为没擦除前父类也没有),所以只能调用子类从父类继承下来的被擦除后的setSecond(Object a)方法,吊用不了重写的setSecond(Date a),相当于是重载了,就达不到目的。那么应该如何解决呢?

要解决这个问题,就需要编译器DateInterval类中生成一个桥方法(bridge method):

3、调用遗留代码

    会有警告是因为:方法setLabelTable的参数类型是Dictionary,而不是Dictionary<Integer,Component>,而我们这里给使用泛型就是因为这个泛型类里的部分属性的类型是类型参数所表示的类型,而方法中没使用泛型,方法就会将传入的泛型类型的对象的那些带类型参数的属性的类型视为Object,所以这就警告我们不要在方法体中进行不当的操作。

六、约束和局限性

其实所有这些局限性,擦除啥的都是为了防止类型转换上的错误,并不影响实际的泛型使用,因为编译器会生成强制类型转换的指令。

1、不能用基本类型实例化类型参数

2、运行时类型查询只适用于原始类型

3、不能创建参数化类型的数组

首先,关于Java的数组:

Java中,任何一个数组都可以向上转型成在定义时指定元素类型的父类的数组,如果在将数组向上转型后,我们往数组里放不同于原始数据类型(即数组定义时的类型)的元素,但是元素的类型满足后来使用的父类类型(即放入的元素的类型是向上转型后的类型的子类)的话,编译不会有问题,但是在运行时会检查加入数组的对象的类型,于是会抛ArrayStoreException(但是若还是放定义时的类型,不会报错):

 

再看Java的泛型:

Java对泛型的支持其实就是在编译器中做了做手脚,增加了一些强制类型转换的代码,也就是说原来需要我们手动写的一些强制类型转换的代码,在泛型的世界里,Java编译器就帮我们做了。

Java的范型停留在编译这一层,到了运行时,这些范型的信息其实是被抹掉的。Java的做法不必修改JVM,减少了潜在的大幅改动和随之而来的风险,也许同时也反映出Java Bytecode规范在设计之初的先天不足。

 

为什么Java不支持创建泛型数组:

因为这样做会破坏类型安全。

因为Java的范型会在运行时将类型信息抹掉,这样如果Java允许我们使用类似

这样的语句的话,我们在随后的代码中可以把它转型为Object[]然后往里面放Map<Double, String>的实例。这样做不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,它能看到的是我们往里面放Map的对象,我们定义的<Integer, String>在这个时候已经被抹掉了,于是而对它而言,只要是Map,都是合法的。想想看,我们本来定义的是装Map<Integer, String>的数组,结果我们却可以往里面放任何Map,接下来如果有代码试图按原有的定义去取值,后果是什么不言自明。(泛型是为了消灭ClassCastException而出现的,但是在这个时候它自己却可能引发ClassCastException,所以就禁止它)

所以,Java编译器不允许我们new泛型数组。

 

另一个例子:

根据上面的分析可以看出来,泛型其实是挺严谨的,说白了就是在"编译的时候通过增加强制类型转换的代码,来避免用户编写出可能引发ClassCastException的代码"。这其实也算是Java引入泛型的一个目的。

但是,一个颇具讽刺意味的问题出现了:如果允许了泛型数组,那么编译器添加的强制类型转换的代码就会有可能是错误的。

看下面的例子:

    上面的代码中,最后一行是重点。因为Java泛型,在编译时通过增加强制类型转换的代码,所以"String value = ref.getValue()"会被替换成"String value = (String)ref.getValue()"。当然我们知道,ref实际上是指向一个存储着StringBuffer对象的GenTest对象。所以,编译器生成出来的代码是隐含着错误的,在运行的时候就会抛出ClassCastException

从这里可以看出,即使支持泛型数组也不会带来什么灾难性的后果,最多就是可能引发ClassCastException。而且平心而论,这个还是程序员自己的错误,实在算不得是Java编译器的错误。但是从另一个角度看,这确实是个巨大的讽刺:泛型是为了消灭ClassCastException而出现的,但是在这个时候它自己却引发了ClassCastException

但是List<String> a = new ArrayList<String>(); 是可以的,因为虽然List是一个容器,但它本质上还是一个类,涉及不到数组的那些转换什么的。

 

    需要说明的是,声明类型为Pair<String>[]的变量仍是合法的,不过不能用new Pair<String>[10]来初始化这个数组变量,即不允许创建(即不能去new)参数化类型的数组(泛型数组)。

4、Varargs警告

上图中的情况也是为什么"需要说明的是,声明类型为Pair<String>[]变量仍是合法的,不过不能用new Pair<String>[10]来初始化这个数组变量,即不允许创建(即不能去new)参数化类型的数组(泛型数组)。"可以声明类型为Pair<String>[]的变量的原因,就是为了在这里使用,又给程序员留了一条小路,但是程序员要严格注意类型转换上的问题。

5、不能实例化类型变量

 

代码转载自:https://pan.quark.cn/s/7f503284aed9 Hibernate的核心组件总数达到五个,具体包括:Session、SessionFactory、Transaction、Query以及Configuration。 这五个核心组件在各类开发项目中都具有普遍的应用性。 借助这些组件,仅可以高效地进行持久化对象的读取与存储,还能够实现事务管理功能。 接下来将通过图形化的方式,逐一阐述这五个核心组件的具体细节。 依据所提供的文件内容,可以总结出以下几个关键知识点:### 1. SSH框架详细架构图尽管标题提及“SSH框架详细架构图”,但在描述部分并未直接呈现关于SSH的详细内容,而是转向介绍了Hibernate的核心接口。 然而,在此我们可以简要概述SSH框架(涵盖Spring、Struts、Hibernate)的核心理念及其在Java开发中的具体作用。 #### Spring框架- **定义**:Spring框架是一个开源架构,其设计目标在于简化企业级应用的开发流程。 - **特点**: - **分层结构**:该框架允许开发者根据实际需求选择性地采纳部分组件,而非强制使用全部功能。 - **可复用性**:Spring框架支持创建可在同开发环境中重复利用的业务逻辑和数据访问组件。 - **核心构成**: - **核心容器**:该部分包含了Spring框架的基础功能,其核心在于`BeanFactory`,该组件通过工厂模式运作,并借助控制反转(IoC)理念,将配置和依赖管理与具体的应用代码进行有效分离。 - **Spring上下文**:提供一个配置文件,其中整合了诸如JNDI、EJB、邮件服务、国际化支持等企业级服务。 - **Spring AO...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值