深入理解Java泛型机制

简介

泛型的意思就是参数化类型,通过使用参数化类型创建的接口、类、方法,可以指定所操作的数据类型。比如:可以使用参数化类型创建操作不同类型的类。操作参数化类型的接口、类、方法成为泛型,比如泛型类、泛型方法。
泛型还提供缺失的类型安全性,我们知道Object是所有类的超类,在泛型前通过使用Object操作各种类型的对象,然后在进行强制类型转换。而通过使用泛型,这些类型转换都是自动或隐式进行的了。因此提高了代码重用能力,而且可以安全、容易的重用代码。

泛型类

<pre>
public class Generic<T> {
T ob;
Generic(T o){
this.ob = o;
}
T getOb(){
return ob;
}
void showType(){
System.out.println("T type:" + ob.getClass().getName());
}
}
</pre>
<pre>
class Generic<T>
</pre>
T是类型参数名称,使用<>括上,这个名称是实际类型的占位符。当创建一个Generic对象的时候,会传递一个实际类型,因为Generic使用了类型参数,所以该类是泛型类。类中只要需要使用类型参数的地方就使用T,当传递实际类型后,会自动改变成实际类型。
比如:
T的类型就是Integer。
<pre>
Generic<Integer> gen1 = new Generic<Integer>(100);
</pre>
T的类型就是String。
<pre>
Generic<String> gen2 = new Generic<String>(“test”);
</pre>

使用泛型类

当调用泛型构造方法时候,仍然需要指定参数类型,因为为构造函数赋值的是Generic<String>。
需要注意上述这个过程,就像Java编译器创建了不同版本的Generic类,但实际编译器并没有那样做,而是将所有泛型类型移除,进行类型转换,从而看似是创建了一个个Generic类版本。移除泛型的过程称为擦除。

泛型只能使用引用类型

当声明泛型实例的时候,传递过来的类型参数必须引用类型。不能是基本类型,比如int、char等。其实可以通过类型封装器封装基本类型,所以这个限制并不严格。
<pre>
Generic<int> gen3 = new Generic<int>();
</pre>

基于不同类型的泛型类是不同的,比如Generic<Integer> gen1和Generic<String> gen2虽然都是Generic<T>类型,但是它们是不同的类型引用,所以gen1 != gen2。这个就是泛型添加类型安全以及防止错误的一部分。

泛型类型安全的原理

上面我们说过,其实泛型的实现完全可以通过使用Object类型替换,将Genneric中所有T转换成Object类型,然后在使用时候通过强制类型转换获取值。但是这有许多风险的,比如手动输入强制类型转换、进行类型检查。而实用泛型它会将这些操作将是隐式完成的,泛型能够保证自动确保类型安全。可以将运行时错误转换成编译时错误,比如如果实用Object替代泛型,对于之前Generic<Integer> gen1 和Generic<String> gen2,将gen1 = gen2这样在泛型中直接编译错误,如果使用Object替代,则不会产生编译错误,因为它们本身都是Generic类型,但是在执行相关代码时候会出错,比如getOb()将String类型直接赋值给int类型。

多个类型参数的泛型类

当需要声明多个参数类型时,只需要使用逗号分隔参数列表即可。
<pre>
public class Generic<T,V> {
T ob1;
V ob2;
Generic(T ob1,V ob2){
this.ob1 = ob1;
this.ob2 = ob2;
}
T getOb1(){
return ob1;
}
V getOb2(){
return ob2;
}
void showType(){
System.out.println("T type:" + ob1.getClass().getName());
System.out.println("V type:" + ob2.getClass().getName());
}
}
</pre>
这样在创建Generic实例时候,需要分别给出参数类型。
<pre>
Generic<String,Integer> generic = new Generic<String,Integer>(“test”,123);
</pre>
泛型类定语法:
<pre>
class class-name<type-param-list>{
//….
}
</pre>
泛型类引用语法:
<pre>
class-name<type-param-list> var-name = new class-name<type-param-list>(con-arg-list);
</pre>

有界类型(bounded type)

前面讨论的泛型,可以被任意类型替换。对于绝大多数情况是没问题的,但是一些特殊场景需要对传递的类型进行限制,比如一个泛型类只能是数字,不希望使用其它类型。我们知道无论Integer还是Double都是Number的子类,所以可以限制只有Number及其子类可以使用,定义的泛型类的时候,在泛型类中可以使用Number中定义的方法(否则无法使用,比如使用Number类中的doubleValue(),如果直接使用会无法通过编译,因为T泛型,并不知道你这个参数类型是什么)。

<pre>
public class Generic<T extends Number> {
T[] array;
Generic(T[] array){
this.array = array;
}
double average(){
double sum = 0;
for(int i=0;i<array.length;i++){
sum += array[i].doubleValue();
}
return sum / array.length;
}
}

Generic<T extends Number>
</pre>

这样T只能被Number及其子类代替,这时候java编译器也知道T类型的对象都可以调用dobuleValue()方法,因为这个方法是Number中定义的。
除了可以使用类作为边界,也可以使用接口作为边界,使用方式与上面相同。同时也可以同时使用一个类和一个接口或多个接口边界,对于这种情况,需要先指定类类型。如果指定接口类型,那么实现了这个接口的类型参数是合法的。
<pre>
class class-name<T extends MyClass & MyInterface>
</pre>

使用通配符参数

我们继续扩展上面这个类,当需要一个sameAvg()方法用来比较两个对象的average()接口是否相同,这个sameAvg()接口怎么写?
第一种方式:
<pre>
boolean sameAvg(Generic<T> ob){
if(average() == ob.average())
return true;
return false;
}
</pre>
这种方式有一个弊端,就是Generic<Integer>只能和Generic<Integer>比较(上面说了),而我们比较相同平均数并care类型。这时我们可以使用通配符“?”来解决。
第二种方式:
<pre>
boolean sameAvg(Generic<?> ob){
//...
}
</pre>

使用通配符需要理解一点,它本身不会影响创建什么类型的Generic对象,通配符只是简单匹配所有有效的(有界类型下的)Generic对象。

有界通配符

使用有界通配符,可以为参数类型指定上界和下界,从而能够限制方法能够操作的对象类型。最常用的是指定有界通配符上界,使用extends子句创建。

<pre>
<? extends superclass>
</pre>
这样直有superclass类及其子类可以使用。也可以指定下界:
<pre>
<? super subclass>
</pre>

这样subclass的超类是可接受的参数类型。
有界通配符的应用场景一般是操作类层次的泛型(C 继承 B,B继承A),控制层次类型。

创建泛型方法

之前讨论泛型类中的泛型方法都是使用创建实例传递过来的类型,其实方法可以本身使用一个或多个类型参数的泛型方法。并且,可以在非泛型类中创建泛型方法。
<pre>
class GenericDemo {
<T extends Comparator<T>, V extends T> boolean isIn(T x, V[] y) {
for (int i = 0; i < y.length; i++) {
if (x.equals(y[i]))
return true;
}
return false;
}
}
</pre>
<pre>
<T extends Comparator<T>, V extends T> boolean isIn(T x, V[] y)
</pre>
泛型参数在返回类型之前,T扩展了类型Comparator<T>,所以只有实现了Comparator<T>接口的类才可以使用。同时V设置了T为上界,这样V必须是T或者其子类。通过强制参数,达到相互兼容。
调用isIn()时候一般可以直接使用,不需要指定类型参数,类型推断就可以自动完成。当然你也可以指定类型:
<pre>
<Integer,Integer>isIn(3,nums);
</pre>
泛型方法语法:
<pre>
<type-param-list> ret-type meth-name(param-list){
//..
}
</pre>
也可以为构造方法泛型化,即便类不是泛型类,但是构造方法是。所以在构造该实例时候需要根据泛型类型给出。
<pre>
<T extends Number> Generic(T a){
//..
}
</pre>

泛型接口

泛型接口与定义泛型类是类似的
<pre>
interface MyInterface<T extends Comparable<T>>{
//...
}
</pre>
当类实现接口时候,因为接口指定界限,所以实现类也需要指定相同的界限。并且接口一旦建立这个界限,那么在实现他的时候就不需要在指定了。
<pre>
class MyClass<T extends Compareable<T>> implements MyInterface<T>{
//...
}
</pre>
如果类实现了具体类型的泛型接口,实现类可以不指出泛型类型。
<pre>
class MyClass implements MyInterface<Integer>{
//...
}
</pre>
使用泛型接口,可以针对不同类型数据进行实现;使用泛型接口也为实现类设置了类型限制条件。
定义泛型接口语法:
<pre>
interface interface-name<type-param-list>{
//...
}
</pre>
实现泛型接口
<pre>
class class-name<type-params-list> implements interface-name<type-arg-list>{
//...
}
</pre>

遗留代码中的原始类型

泛型是在JDK 5之后提供的,在JDK 5之前是不支持的泛型的。所以这些遗留代码即需要保留功能,又要和泛型兼容。可以使用混合编码,还比如上面的例子。Generic<T> 类是一个泛型类,我们可以使用原始类型(不指定泛型类型),来创建Generic类。
<pre>
Generic gen1 = new Generic(new Double(9.13));
double gen2 = (Double)gen1.getOb();
</pre>
java是支持这种原始类型,然后通过强制类型转换使用的。但是正如我们上面说的,这就绕过了泛型的类型检查,它是类型不安全的,有可能导致运行时异常(RunTime Exception)。

泛型类层次

泛型类也可以是层次的一部分,就像非泛型类那样。泛型类可以作为超类或子类。泛型和非泛型的区别在于,泛型类层次中的所有子类会将类型向上传递给超类。
<pre>
class Gen<T>{
T ob;
Gen(T ob){
this.ob = ob;
}
T genOb(){
return ob;
}
}
class Gen2<T> extends Gen<T>{
Gen2(T o){
super(o);//向上传递
}
}
</pre>
<pre>
Gen2<Integer> gen = new Gen2<Integer>();
</pre>
创建Gen2传入Integer类型,Integer类型也会传入超类Gen中。子类可以根据自己的需求,任意添加参数类型。
<pre>
class Gen2<T,V> extends Gen<T>{
Gen2(T a,V b){
super(a);//一定要有
}
}
</pre>
超类也可以不是泛型,子类在继承的时候,就不需要有特殊的条件了。
<pre>
class Gen{
Gen(int a){
}
}
class Gen2<T,V> extends Gen{
Gen2(T a,int b){
super(b);
}
Gen2(T a,V b,int c){
super(c);
}
}
</pre>
需要注意的:

  • 泛型类型强制类型转换,需要两个泛型实例的类型相互兼容并且它们的类型参数也相同。
  • 可以向重写其它方法那样重写泛型的方法。
  • 从JDK 7起泛型可以使用类型推断在创建实例时候省略类型,因为在参数声明的时候已经指定过一次了,所以可以根据声明的变量进行类型推断。
    List<String,Integer> list = new ArrayList<>();

擦除

泛型为了兼容以前的代码(JDK 5之前的),使用了擦除实现泛型。具体就是,当编译java代码的时候,所有泛型信息被移除(擦除)。会使用它们的界定类型替换,如果没有界定类型,会使用Object,然后进行适当的类型转换。

模糊性错误

泛型引入后,也增加了一种新类型错误-模糊性错误的可能,需要进行防范。当擦除导致两个看起来不同的泛型声明,在擦除之后可能变成相同类型,从而导致冲突。
<pre>
class Gen<T,V>{
T ob1;
V ob2;
void setOb(T ob){
this.ob1 = ob;
}
void setOb(V ob){
this.ob2 = ob;
}
}
</pre>
这种是无法编译的,因为当擦除后可能会导致类型相同,这样的方法重载是不对的。
<pre>
Gen<String,String> gen = new Gen<String,String>();
</pre>
这样T和V都是String类型,明显代码是不对的。
可以通过指定一个类型边界,比如:
<pre>
class Test1{
public static void main(String[] args){
//没问题
Gen<String,Integer> gen = new Gen<String, Integer>();
gen.setOb(1);
//这样在调用setOb的时候也会编译失败,因为都为Integer类型,方法重载错误
Gen<Integer,Integer> gen1 = new Gen<Integer, Integer>();
gen1.setOb(1);
}
}
</pre>
所以在解决这种模糊错误时候,最好使用独立的方法名,而不是去重载。

使用泛型的限制

  • 不能实例化类型参数,因为编译器不知道创建哪种类型,T只是类型占位符。
    <pre>
    class Gen<T>{
    T ob;
    Gen(){
    ob = new T();
    }
    }
    </pre>
  • 静态成员不能使用类中声明的类型参数。
    <pre>
    class Gen<T>{
    //错误的,不能声明静态成员
    static T ob;
    //错误的,静态方法不能使用参数类型T
    static T getGen(){
    return ob;
    }
    //正确的,静态方法不是参数类型
    static void printXXX(){
    System.out.println();
    }
    }
    </pre>
  • 不能实例化类型参数数组
    <pre>
    //没问题
    T[] vals;
    //不能实例化类型参数数组
    vals = new T[10];
    </pre>
  • 不能创建特性类型的泛型应用数组
    <pre>
    //这是不允许的
    Gen<Integer> gen = new Gen<Integer>[10];
    但是可以使用通配符,并且比使用原始类型好,因为进行了类型检查。
    Gen<?> gen = new Gen<?>[10];
    </pre>
  • 泛型类不能扩展Throwable,这就意味着不嗯滚创建泛型异常类。

关注我

欢迎关注我的公众号,会定期推送优质技术文章,让我们一起进步、一起成长!
公众号搜索:data_tc
或直接扫码:🔽


欢迎关注我
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 在之前的文章中分析过了多态,可以知道多态本身是一种泛化机制,它通过基类或者接口来设计,使程序拥有一定的灵活性,但是...
    _小二_阅读 677评论 0 0
  • object 变量可指向任何类的实例,这让你能够创建可对任何数据类型进程处理的类。然而,这种方法存在几个严重的问题...
    CarlDonitz阅读 908评论 0 5
  • Java泛型总结# 泛型是什么## 从本质上讲,泛型就是参数化类型。泛型十分重要,使用该特性可以创建类、接口以及方...
    kylinxiang阅读 908评论 0 1
  • 今年春节假期参与了郑州十点读书会组织的共读活动,我读的是蒋勋老师的《品味四讲》,一共用了十天的时间才断断续续的看完...
    正齐读道阅读 626评论 3 0
  • 下午户外活动是夹球跳,龙龙经过前几周玩耍的经验龙龙已经掌握夹球的秘诀啦,刚拿到球就能轻松的把球给夹起来,并夹着球跳...
    a81c671c0ae2阅读 272评论 0 0