Java泛型程序设计

Java中对类进行设计时可以在设计类的声明期在所设计的类上声明参数,设计有类型参数的类就叫做泛型类,类型参数就称为类的泛型。除了能在类上声明泛型外,Java还可以在类的方法上声明泛型,这种在类的方法上有泛型声明的方法就叫做泛型方法。泛型表达式是“<>”括住类型参数的声明,形如:public class A<T>{ public <H> void a(H h){}}。其中“public <H> void a(H h){}”是泛型方法的声明表现形式。

概述

Java 平台在JDK 5.0后为Java编程语言扩展了很多新的特性,泛型是这些重要特性中的一个。泛型的出现使程序员设计出更抽象的数据类型(更抽象意味着更一般化,更一般化意味着更通用,更通用就是更简易地复用)成为了可能。

我们先来看看不使用泛型的List集合的基本操作简单的程序设计实现过程。

List store=new ArrayList();             //①

store.add(new Integer(12));           // ②

Integer data = (Integer)store.get(0);//③

 

代码①处创建了一个List对象store,②处将整型数据12存储于store中,③处将整数12从store中取出来(目前store中只有一个元素所以使用0索引值就可以将12取出来)。③处的代码:(Integer)store.get(0),“(Integer)”强制类型转换是必须的,因为JVM在编译期会做数据类型安全检测的,此时store.get(0)返回是一个Object类型对象,Object类型是Java平台所有类的父类,如果没有(Integer)显示地类型转换,JVM就不会查找到Object对象的引用类型对象Integer的实例,编译期JVM就会报类型不兼容的错:

 

再来看看使用了泛型的List的简单实现过程:

List<Integer> store = new ArrayList<Integer>();//①

store.add(new Integer(12));                             // ②

Integer data = store.get(0);                            //③

 

我们可以发现③的代码不再需要“(Integer)”的强制类型转换就能通过Java编译器类型安全的检测,从而最终能通过Java编译器的编译。

 

一、Java的泛型声明规范及约束:

  • 设计时声明的泛型参数不能是具体的类(所谓具体的类就是已经编译好形成class字节码的类),而只能是以形参占位符的形式出现。
  • 只有形参占位符的类不能实例化形成对象,泛型类的实例化只能在为泛型类传入了具体类型后方可构造实例对象。
  • 没有形参类的泛型类的数组。
  • 类的静态字段域无法声明形参类的泛型类。静态方法不能直接使用带泛型形参的泛型类,除非该静态方法被设计为泛型方法。
  • 泛型参数代表的是Java的复合对象类型,为泛型类传参时不能是元数据类型(即Java的8种基本数据类型)。
  • 泛型参数可以声明限定约束,有限定约束的泛型参数用extends关键字标识:public class A<T extends Serializable>,该声明约束的语义是:类A的泛型参数T必须是继承或实现了Serializable接口的类型。extends关键字后可以是多个类,多个类以“&”符号分隔。
  • 泛型形参声明可以是一个也可以是多个,多个形参以英文环境的”,”分割,且”,”不能出现在形参列表的第一和最后一位。多个形参标识符序列在”<>”中都是唯一的、大小写敏感的、不能重复出现的标识符。
  • 在泛型声明设计完成后,在使用该类时,为泛型类传递具体类型,如果不确定要传哪个具体类型,这时可以使用“?”通配符暂时代替;“?”表明该类是个泛型类,但参数类型目前还不确定,使用“?”可以匹配任意的类型,该类型要到运行时由具体的类型实参匹配决定;每个类上可以出现一次或多次“?”,但每个“?”只能匹配一个类型实参。
  • 通配符“?”后可以出现extends和super关键字,extends的语义和类型声明时一样的;而super表明“?”匹配的是super后面的类的父类并包括输入实参本身都可匹配。super只能出现在类的字段域和方法参数域中的“<>”中。
  • 泛型方法的泛型声明必须在方法的返回值前声明。如果没有什么特别要求建议泛型方法的泛型形参不要和该方法所在的类声明的泛型形参(如果有的话)一样。
  • 泛型形参标识符在使用上基本没什么要求,只要是合法的、不与Java关键字冲突的标识符都可以用,但建议尽量使用单个大写的英文字母作为泛型形参的标识符。
  • try{}catch(){}块中catch的“()”中不能出现泛型参数,但可以使用泛型方法声明方法抛出的异常类:

合法的:

<T extends Throwable> doWork(T t) throws T{

     try{

}catch(Throwable e){

    e.printStackTrace();

    throw t;

}

}

不合法的:

<T extends Throwable> doWork(){

     try{

}catch(T e){

    e.printStackTrace();

}

}

  • 所有泛型类的泛型形参都在编译期接受Java编译器的类型安全检测并标记在类的“.class”文件中,在JVM中不会存在“A<X,X,X…>”或“A<?>”的对象形式,在JVM中泛型类最终由Object对象多态引用,这就是所谓的泛型“擦除”机制。

 

二、泛型程序设计初步

现在我们通过一个简单程序设计入手,初步感受下泛型程序是如何设计的。

比如现在我们想设计一个类来比较两个数的大小,并返回较大的那个数,我们的代码如下:

codes_0

 

类CompareNumber定义了一方法 max比较两个整数,并返回较大的数,此方法是可运行的符合Java方法定义规范方法,但此方法在通用性的设计的需求上是有缺陷的。我们知道,既然是数字的比较,不单整数可以比较,小数也可比较,而且在Java中数字的类型除了整数还有:Byte,Short,Float,Double和Long类型的数字,如果某个时期我们需要比较所有数字类型的数据大小,我们的CompareNumber类就应该重新设计为:

codes_1

 

codes_1中我们为每个数字类型都“重载”max方法,这个类满足了所有数字类型数据的比较,同时我们发现为了比较数字大小,我们不得不为每种数字类型都设计一个方法,Java中有6种数字类型,就得“重载”6种类型的方法,这无疑增加了程序员的代码书写量,但如果我们使用泛型来设计CompareNumber类就会大大减少CompareNumber的代码量:

codes_2:

 

 

codes_2相比于codes_1少写了5个方法,只是在CompareNumber类型声明的地方增加了<T extends Number>泛型定义,就能满足code_1所有的需求,现在可以写个带main方法的类:CompareNumberMain来验证我们的需求设计目标:

codes_3:

 

codes_3的CompareNumberMain类实现了Java的main方法,main方法的8~13行代码分别实现CompareNumber的Double,Long,Integer,Float,Short,Bye泛型限定的对象,15~20行代码分别调用各个泛型的max方法,22~27行分别打印输出max的执行结果:

 

可以发现各个方法都返回了比较的数字较大的的那一个。

<T extends Number>的声明表明CompareNumber类只处理Number类型及其子类类型的数据,如果在调用max方法时传入的是String和Character或其他不是Number类型的数据,编译器在编译的时候就会报错。<T extends Number>不但增强了程序设计时程序的可读性,且使设计的类有了更强的类型安全性。

对于设计两个数的比较返回更大数需求目标的程序设计,CompareNumber<T extends Number>的设计完全还可以设计得更简洁一点,那就是使用泛型方法的形式。

codes_4

 

codes_4的第5行我们将max声明为泛型方法,声明的的形参<T extends Number>必须在方法返回值声明的前边的位置。现在CompareNumber在使用上就有所改变:

codes_5

 

codes_5相比于codes_4,我们只需在代码第7行创建一个CompareNumber实例,比codes_4足足少了5行代码。在调用max方法时传入不同的数字类型数据,同样会返回比较后的较大的数字类型数据:

 

从上面的设计比较来看,似乎泛型方法总比泛型类的形式简洁,那以后在设计时只需设计泛型方法就足够了?其实不尽然,Java中方法总是隶属于某个类的,且方法运行完成后,方法体的状态不会在内存中存留,即便是泛型方法也遵循这一规范,而一个类对象却可以重复“钝化、激活”、“激活、钝化”,类在内存中的状态(类的对象的状态)取决于对垃圾回收器的管理,即便是类的泛型参数对象也遵循这一规范。所以抉择将功能设计为泛型类还是泛型方法要依据实际业务需求场景目标来定:如果功能状态不是持续复用,就将功能设计为泛型方法,如果一次运行还要保存状态为下一次复用,是类或泛型类在Java中存在的根本意义。

三、泛型通配符“?”的使用

当泛型类被某个类引用时(引用可以在类的字段域和方法的参数序列中),可以使用通配符作为泛型类的泛型形参,这样在运行时,JVM就可以通过传入的实际参数的类型“推断”出引用的泛型类形参的实际类型。

jdk5后Class类已经改写为具有泛型声明的类:

 

Type Parameters:

T - the type of the class modeled by this Class object. For example, the type of String.class is Class<String>. Use Class<?> if the class being modeled is unknown.

All Implemented Interfaces:

Serializable, AnnotatedElement, GenericDeclaration, Type .

 

上面一段英文来自Java8的API文档关于Class类的描述,大概意思是泛型参数T是class对象的类型参数,比如String.class就是Class<String>,如果在创建Class对象时不确定Class泛型参数时就使用Class<?>替代。看这段英文即使英文很好还是觉得很抽象,下面我们通过一段代码实例来体会下泛型通配符“?”的使用。java.lang.reflect包下的Array工具类的newInstance(Class<?> componentType, int length)就使用到Class<?>,我们就用它吧。

codes_6:

 

 

 

代码codes_6的第7~9行对Array.newInstance进行了简单包装,代码13~14分别创建int类型的数组a,a数组的长度为3;和String类型的数组sa,sa长度为5,我们先看看程序运行的结果:

 

可以发现数组a对象和数组sa对象都能创建各自的长度分别为3和5。Class<?>可以匹配各种Class类的对象给Array的newInstance方法使用,并创建不同类型的数组对象实例,包括您自己定义的类对象的数组。

 

 

代码15行为Person类创建一个长度为8的Person数组,程序运行的结果为:

 

有些书在写到Array动态创建类型数组时,看到Array.newInstance返回的是:Object对象,每次创建数组都要强制类型转换,建议重新包装Array.newInstance为:

 

 

 

如果都是用于匹配对象类型的数组,ArrayDemo的newInstance方法没有任何问题,可一旦传入8种基本数据类型的数组时编译器就会报错:

 

无论变换成byte、short、float、char、double、long还是boolean,Java编译器都会报类型不兼容的错误。究其原因,因为Java的八种基本数据类型不是对象类型,而代码13行newInstance方法参数列表中的Class<T>的类型形参T只能匹配对象类型,而byte、short、float、int、 char、double、long和boolean并不是对象类型,不是T形参所要接收的实参类型,出于“强类型”编程语言的类型安全的考虑,Java编译器是不会让代码编译通过的。只有Class<?>可以匹配Java任意的“.class”对象甚至void.class。且虽然byte、short、float、char、double、long和boolean不是对象类型,但它们的数组却是对象类型的;T只是类型形参,在JVM的运行环境中,终将“<T>”“擦除”而委托Object对象来引用,不管是String还是String[]或者Person及Person[]都能将类型转换为Object都是Object的子类,当然也包括byte[]、short[]、float[]、char[]、double[]、long[]和boolean[]等数组们,而“T[]”中的“T”确定表明只能是Java的对象类型,无法将byte、short、float等传递给“T[]”中的“T”的,所以只能将代码13~15行的方法改为下面的设计,就能兼顾所有类型数组的动态创建:

codes_6_1

 

 

 codes_6_1中如果要创建二维或多维数组可以给参数变量type传递byte[].class、byte[][].class等形式的变量来创建。

 

四、捕获通配符“?”的类型变量

在使用泛型通配符“?”的时候,必须注意:A<?>毕竟不是一个具体的类,A<?>.class对象在Java中是不存在的,只有A.class。所有在设计带泛型通配符“?”程序时都要注意<?>类型变量的捕获。那什么是<?>类型变量的捕获呢?

我们都知道List JDK5后也已经改写为泛型类了:

 

比如我们想交换List中0索引和1索引的元素,我们的初始设计是这样的:

cdoes_7:

 

codes_7中代码第8行设计了个泛型方法:swap,在该方法中对list的0索引元素和1索引元素进行交换,如代码9~12行所示。程序运行的结果如下:

 

现在将swap方法改为List<?>的方式:

 

代码第8~9行之所以使用Object变量是因为List<?>中存储的具体类型为:“不确定”,所以使用Object变量是个不得已的权宜之计。

在编译时Java编译器就会报如下错误:

 

可以看到无法捕获“?”类型变量的错误提示。要捕获“?”的类型变量其实从codes_7的泛型方法可以得到启示的。public static <E> void swap(List<E> list)中的参数变量list可以接收任意的List集合对象,如果给list传递的是List<String>表明list的List集合就是处理String类型的集合,如果传递的是List<Integer>就表明list的集合就是处理Integer类型的集合。

codes_8:

 

代码codes_8的swapBridge方法声明为泛型方法,该泛型方法的语义是:泛型参数<E>代表任意Java对象类型,相当于Object对象,可以接收任意类型对象类型给方法的List<E>集合使用,方法参数变量list“等待”调用者传入实参。swap方法的形参List<?>表明变量list是一个List集合,而集合存储的类型参数要“等待”实际参数传入后才能匹配,匹配的结果由swapBridge的List<E>推断后得出具体的类型,就可根据具体类型上定义的方法来实现业务逻辑。codes_8和code_7的执行结果没有什么区别:

 

 

通过上面几节简要的阐述和代码实践我们得到以下结论:

1.泛型的引入可以减少业务逻辑实现的代码量,增强了代码的可读性;

2.丰富了类型定义类型引用的多种途径,给应用程序增添了业务封装的灵活性;

3强大的应用程序的封装性,大大增强了程序的内聚性,为代码的复用和组件化提供了可用的基础。

 

转载于:https://www.cnblogs.com/gfblog/p/10338401.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值