java泛型详解-绝对是对泛型⽅法讲解最详细的,没有之⼀
对java的泛型特性的了解仅限于表⾯的浅浅⼀层,直到在学习设计模式时发现有不了解的⽤法,才想起详细的记录⼀下。
本⽂参考java泛型详解、Java中的泛型⽅法、java泛型详解
1.概述
泛型在java中有很重要的地位,在⾯向对象编程及各种设计模式中有⾮常⼴泛的应⽤。
什么是泛型?为什么要使⽤泛型?
泛型,即“参数化类型”。⼀提到参数,最熟悉的就是定义⽅法时有形参,然后调⽤此⽅法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类
型由原来的具体的类型参数化,类似于⽅法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使⽤/调⽤时传⼊具体的类型
(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使⽤过程中,操
作的数据类型被指定为⼀个参数,这种参数类型可以⽤在类、接⼝和⽅法中,分别被称为泛型类、泛型接⼝、泛型⽅法。
2.⼀个栗⼦
⼀个被举了⽆数次的例⼦:
ListarrayList=newArrayList();
("aaaa");
(100);
for(inti=0;i<();i++){
Stringitem=(String)(i);
Log.d("泛型测试","item="+item);
}
毫⽆疑问,程序的运⾏结果会以崩溃结束:
astException:
ArrayList可以存放任意类型,例⼦中添加了⼀个String类型,添加了⼀个Integer类型,再使⽤时都以String的⽅式使⽤,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可
以解决),泛型应运⽽⽣。
我们将第⼀⾏声明初始化list的代码更改⼀下,编译器会在编译阶段就能够帮我们发现类似这样的问题。
List
...
//(100);在编译阶段,编译器就会报错
3.特性
泛型只在编译阶段有效。看下⾯的代码:
List
List
ClassclassStringArrayList=ss();
ClassclassIntegerArrayList=ss();
if((classIntegerArrayList)){
Log.d("泛型测试","类型相同");
}
输出结果:D/泛型测试:类型相同。
通过上⾯的例⼦可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,
并且在对象进⼊和离开⽅法的边界处添加类型检查和类型转换的⽅法。也就是说,泛型信息不会进⼊到运⾏时阶段。
对此总结成⼀句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
4.泛型的使⽤
泛型有三种使⽤⽅式,分别为:泛型类、泛型接⼝、泛型⽅法
4.1泛型类
泛型类型⽤于类的定义中,被称为泛型类。
通过泛型可以完成对⼀组类的操作对外开放相同的接⼝。最典型的就是各种容器类,如:List、Set、Map。
泛型类的最基本写法(这么看可能会有点晕,会在下⾯的例⼦中详解):
class类名称<泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private泛型标识/*(成员变量类型)*/var;
.....
}
⼀个最普通的泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常⽤于表⽰泛型
//在实例化泛型类时,必须指定T的具体类型
publicclassGeneric
//key这个成员变量的类型为T,T的类型由外部指定
privateTkey;
publicGeneric(Tkey){//泛型构造⽅法形参key的类型也为T,T的类型由外部指定
=key;
}
publicTgetKey(){//泛型⽅法getKey的返回值类型为T,T的类型由外部指定
returnkey;
}
}
//泛型的类型参数只能是类类型(包括⾃定义类),不能是简单类型
//传⼊的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic
//传⼊的实参类型需与泛型的类型参数类型相同,即为String.
Generic
Log.d("泛型测试","keyis"+());
Log.d("泛型测试","keyis"+());
12-2709:20:04.43213063-13063/?D/泛型测试:keyis123456
12-2709:20:04.43213063-13063/?D/泛型测试:keyiskey_vlaue
定义的泛型类,就⼀定要传⼊泛型类型实参么?并不是这样,在使⽤泛型的时候如果传⼊泛型实参,则会根据传⼊的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作⽤。
如果不传⼊泛型类型实参的话,在泛型类中使⽤泛型的⽅法或成员变量定义的类型可以为任何的类型。
看⼀个例⼦:
Genericgeneric=newGeneric("111111");
Genericgeneric1=newGeneric(4444);
Genericgeneric2=newGeneric(55.55);
Genericgeneric3=newGeneric(fal);
Log.d("泛型测试","keyis"+());
Log.d("泛型测试","keyis"+());
Log.d("泛型测试","keyis"+());
Log.d("泛型测试","keyis"+());
D/泛型测试:keyis111111
D/泛型测试:keyis4444
D/泛型测试:keyis55.55
D/泛型测试:keyisfal
注意:
泛型的类型参数只能是类类型,不能是简单类型。
不能对确切的泛型类型使⽤instanceof操作。如下⾯的操作是⾮法的,编译时会出错。
if(ex_numinstanceofGeneric){
....
}
4.2泛型接⼝
泛型接⼝与泛型类的定义及使⽤基本相同。泛型接⼝常被⽤在各种类的⽣产器中,可以看⼀个例⼦:
//定义⼀个泛型接⼝
publicinterfaceGenerator
publicTnext();
}
当实现泛型接⼝的类,未传⼊泛型实参时:
/**
*未传⼊泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也⼀起加到类中
*即:classFruitGenerator
*如果不声明泛型,如:classFruitGeneratorimplementsGenerator
*/
classFruitGenerator
@Override
publicTnext(){
returnnull;
}
}
当实现泛型接⼝的类,传⼊泛型实参时:
/**
*传⼊泛型实参时:
*定义⼀个⽣产器实现这个接⼝,虽然我们只创建了⼀个泛型接⼝Generator
*但是我们可以为T传⼊⽆数个实参,形成⽆数种类型的Generator接⼝。
*在实现类实现泛型接⼝时,如已将泛型类型传⼊实参类型,则所有使⽤泛型的地⽅都要替换成传⼊的实参类型
*即:Generator
*/
publicclassFruitGeneratorimplementsGenerator
privateString[]fruits=newString[]{"Apple","Banana","Pear"};
@Override
publicStringnext(){
Randomrand=newRandom();
returnfruits[t(3)];
}
}
4.3泛型通配符
我们知道Integer是Number的⼀个⼦类,同时在特性章节中我们也验证过Generic
那么问题来了,在使⽤Generic
在逻辑上类似于Generic
为了弄清楚这个问题,我们使⽤Generic
publicvoidshowKeyValue1(Generic
Log.d("泛型测试","keyvalueis"+());
}
Generic
Generic
showKeyValue(gNumber);
//showKeyValue这个⽅法编译器会为我们报错:Generic
//cannotbeappliedtoGeneric<>
//showKeyValue(gInteger);
通过提⽰信息我们可以看到Generic
由此可以看出:同⼀种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
回到上⾯的例⼦,如何解决上⾯的问题?总不能为了定义⼀个新的⽅法来处理Generic
因此我们需要⼀个在逻辑上可以表⽰同时是Generic
我们可以将上⾯的⽅法改⼀下:
publicvoidshowKeyValue1(Generic<?>obj){
Log.d("泛型测试","keyvalueis"+());
}
类型通配符⼀般是使⽤?代替具体的类型实参,注意了,此处’?’是类型实参,⽽不是类型形参。
重要说三遍!此处’?’是类型实参,⽽不是类型形参!此处’?’是类型实参,⽽不是类型形参!
再直⽩点的意思就是,此处的?和Number、String、Integer⼀样都是⼀种实际的类型,可以把?看成所有类型的⽗类。是⼀种真实的类型。
可以解决当具体类型不确定的时候,这个通配符就是?;
当操作类型时,不需要使⽤类型的具体功能时,只使⽤Object类中的功能。那么可以⽤?通配符来表未知类型。
4.4泛型⽅法
在java中,泛型类的定义⾮常简单,但是泛型⽅法就⽐较复杂了。
尤其是我们见到的⼤多数泛型类中的成员⽅法也都使⽤了泛型,有的甚⾄泛型类中也包含着泛型⽅法,这样在初学者中⾮常容易将泛型⽅法理解错了。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型⽅法,是在调⽤⽅法的时候指明泛型的具体类型。
/**
*泛型⽅法的基本介绍
*@paramtClass传⼊的泛型实参
*@returnT返回值为T类型
*说明:
*1)public与返回值中间
*2)只有声明了
*3)
*4)与泛型类的定义⼀样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常⽤于表⽰泛型。
*/
public
Tinstance=tance();
returninstance;
}
Objectobj=genericMethod(e(""));
4.4.1泛型⽅法的基本⽤法
光看上⾯的例⼦有的同学可能依然会⾮常迷糊,我们再通过⼀个例⼦,把我泛型⽅法再总结⼀下。
publicclassGenericTest{
//这个类是个泛型类,在上⾯已经介绍过
publicclassGeneric
privateTkey;
publicGeneric(Tkey){
=key;
}
//我想说的其实是这个,虽然在⽅法中使⽤了泛型,但是这并不是⼀个泛型⽅法。
//这只是类中⼀个普通的成员⽅法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个⽅法中才可以继续使⽤T这个泛型。
publicTgetKey(){
returnkey;
}
/**
*这个⽅法显然是有问题的,在编译器会给我们提⽰这样的错误信息"cannotreslovesymbolE"
*因为在类的声明中并未声明泛型E,所以在使⽤E做形参和返回值类型时,编译器会⽆法识别。
publicEtKey(Ekey){
=keu
}
*/
}
/**
*这才是⼀个真正的泛型⽅法。
*⾸先在public与返回值之间的
*这个T可以出现在这个泛型⽅法的任意位置.
*泛型的数量也可以为任意多个
*如:public
*...
*}
*/
public
n("containerkey:"+());
//当然这个例⼦举的不太合适,只是为了说明泛型⽅法的特性。
Ttest=();
returntest;
}
//这也不是⼀个泛型⽅法,这就是⼀个普通的⽅法,只是使⽤了Generic
publicvoidshowKeyValue1(Generic
Log.d("泛型测试","keyvalueis"+());
}
//这也不是⼀个泛型⽅法,这也是⼀个普通的⽅法,只不过使⽤了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是⼀种类型实参,可以看做为Number等所有类的⽗类
publicvoidshowKeyValue2(Generic<?>obj){
Log.d("泛型测试","keyvalueis"+());
}
/**
*这个⽅法是有问题的,编译器会为我们提⽰错误信息:"UnKnownclass'E'"
*虽然我们声明了
*但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public
...
}
*/
/**
*这个⽅法也是有问题的,编译器会为我们提⽰错误信息:"UnKnownclass'T'"
*对于编译器来说T这个类型并未项⽬中声明过,因此编译也不知道该如何编译这个类。
*所以这也不是⼀个正确的泛型⽅法声明。
publicvoidshowkey(TgenericObj){
}
*/
publicstaticvoidmain(String[]args){
}
}
4.4.2类中的泛型⽅法
当然这并不是泛型⽅法的全部,泛型⽅法可以出现杂任何地⽅和任何场景中使⽤。但是有⼀种情况是⾮常特殊的,当泛型⽅法出现在泛型类中时,我们再通过⼀个例⼦看⼀下
publicclassGenericFruit{
classFruit{
@Override
publicStringtoString(){
return"fruit";
}
}
classAppleextendsFruit{
@Override
publicStringtoString(){
return"apple";
}
}
classPerson{
@Override
publicStringtoString(){
return"Person";
}
}
classGenerateTest
publicvoidshow_1(Tt){
n(ng());
}
//在泛型类中声明了⼀个泛型⽅法,使⽤泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型⽅法在声明的时候会声明泛型
public
n(ng());
}
//在泛型类中声明了⼀个泛型⽅法,使⽤泛型T,注意这个T是⼀种全新的类型,可以与泛型类中声明的T不是同⼀种类型。
public
n(ng());
}
}
publicstaticvoidmain(String[]args){
Appleapple=newApple();
Personperson=newPerson();
GenerateTest
//apple是Fruit的⼦类,所以这⾥可以
_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,⽽传⼊的实参类是Person
//_1(person);
//使⽤这两个⽅法都可以成功
_2(apple);
_2(person);
//使⽤这两个⽅法也都可以成功
_3(apple);
_3(person);
}
4.4.3泛型⽅法与可变参数
再看⼀个泛型⽅法和可变参数的例⼦:
public
for(Tt:args){
Log.d("泛型测试","tis"+t);
}
}
printMsg("111",222,"aaaa","2323.4",55.55);
4.4.4静态⽅法与泛型
静态⽅法有⼀种情况需要注意⼀下,那就是在类中的静态⽅法使⽤泛型:静态⽅法⽆法访问类上定义的泛型;如果静态⽅法操作的引⽤数据类型不确定的时候,必须要将泛型定义在⽅
法上。
即:如果静态⽅法要使⽤泛型的话,必须将静态⽅法也定义成泛型⽅法。
publicclassStaticGenerator
....
....
/**
*如果在类中定义使⽤泛型的静态⽅法,需要添加额外的泛型声明(将这个⽅法定义成泛型⽅法)
*即使静态⽅法要使⽤泛型类中已经声明过的泛型也不可以。
*如:publicstaticvoidshow(Tt){..},此时编译器会提⽰错误信息:
"StaticGeneratorcannotberefrencedfromstaticcontext"
*/
publicstatic
}
}
4.4.5泛型⽅法总结
泛型⽅法能使⽅法独⽴于类⽽产⽣变化,以下是⼀个基本的指导原则:
⽆论何时,如果你能做到,你就该尽量使⽤泛型⽅法。也就是说,如果使⽤泛型⽅法将整个类泛型化,那么就应该使⽤泛型⽅法。另外对于⼀个static的
⽅法⽽已,⽆法访问泛型类型的参数。
所以如果static⽅法要使⽤泛型能⼒,就必须使其成为泛型⽅法。
4.5泛型上下边界
在使⽤泛型的时候,我们还可以为传⼊的泛型类型实参进⾏上下边界的限制,如:类型实参只准传⼊某种类型的⽗类或某种类型的⼦类。
为泛型添加上边界,即传⼊的类型实参必须是指定类型的⼦类型
publicvoidshowKeyValue1(Generic<?extendsNumber>obj){
Log.d("泛型测试","keyvalueis"+());
}
Generic
Generic
Generic
Generic
//这⼀⾏代码编译器会提⽰错误,因为String类型并不是Number类型的⼦类
//showKeyValue1(generic1);
showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);
如果我们把泛型类的定义也改⼀下:
publicclassGeneric
privateTkey;
publicGeneric(Tkey){
=key;
}
publicTgetKey(){
returnkey;
}
}
//这⼀⾏代码也会报错,因为String不是Number的⼦类
Generic
1
2
再来⼀个泛型⽅法的例⼦:
//在泛型⽅法中添加上下边界限制的时候,必须在权限声明与返回值之间的
//public
public
n("containerkey:"+());
Ttest=();
returntest;
}
通过上⾯的两个例⼦可以看出:泛型的上下边界添加,必须与泛型的声明在⼀起。
4.6关于泛型数组要提⼀下
看到了很多⽂章中都会提起泛型数组,经过查看Sun的说明⽂档,在java中是”不能创建⼀个确切的泛型类型的数组”的。
也就是说下⾯的这个例⼦是不可以的:
List
⽽使⽤通配符创建泛型数组是可以的,如下⾯这个例⼦:
List<?>[]ls=newArrayList<?>[10];
这样也是可以的:
List
下⾯使⽤Sun的⼀篇⽂档的⼀个例⼦来说明这个问题:
List
Objecto=lsa;
Object[]oa=(Object[])o;
List
(newInteger(3));
oa[1]=li;//Unsound,butpassruntimestorecheck
Strings=lsa[1].get(0);//Run-timeerror:ClassCastException.
这种情况下,由于JVM泛型的擦除机制,在运⾏时JVM是不知道泛型信息的,所以可以给oa[1]赋上⼀个ArrayList⽽不会出现异常,但是在取出数据的时
候却要做⼀次类型转换,所以就会出现ClassCastException,如果可以进⾏泛型数组的声明,上⾯说的这种情况在编译期将不会出现任何的警告和错
误,只有在运⾏时才会出错。
⽽对泛型数组的声明进⾏限制,对于这样的情况,可以在编译期提⽰代码有类型安全问题,⽐没有任何提⽰要强很多。
下⾯采⽤通配符的⽅式是被允许的:数组的类型不可以是类型变量,除⾮是采⽤通配符的⽅式,因为对于通配符的⽅式,最后取出数据是要做显式的类型转换的。
List<?>[]lsa=newList<?>[10];//OK,arrayofunboundedwildcardtype.
Objecto=lsa;
Object[]oa=(Object[])o;
List
(newInteger(3));
oa[1]=li;//Correct.
Integeri=(Integer)lsa[1].get(0);//OK
5.最后
本⽂中的例⼦主要是为了阐述泛型中的⼀些思想⽽简单举出的,并不⼀定有着实际的可⽤性。另外,⼀提到泛型,相信⼤家⽤到最多的就是在集合中,其实,在实际的编程过程中,⾃
⼰可以使⽤泛型去简化开发,且能很好的保证代码质量。
本文发布于:2022-12-07 12:20:05,感谢您对本站的认可!
本文链接:http://www.wtabcd.cn/fanwen/fan/88/59537.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |