深入理解Java泛型机制

简介

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

泛型类


public class Generic {
T ob;
Generic(T o){
this.ob = o;
}
T getOb(){
return ob;
}
void showType(){
System.out.println("T type:" + ob.getClass().getName());
}
}


class Generic

T是类型参数名称,使用<>括上,这个名称是实际类型的占位符。当创建一个Generic对象的时候,会传递一个实际类型,因为Generic使用了类型参数,所以该类是泛型类。类中只要需要使用类型参数的地方就使用T,当传递实际类型后,会自动改变成实际类型。
比如:
T的类型就是Integer。

Generic gen1 = new Generic(100);

T的类型就是String。

Generic gen2 = new Generic(“test”);

使用泛型类

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

泛型只能使用引用类型

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


Generic gen3 = new Generic();

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

泛型类型安全的原理

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

多个类型参数的泛型类

当需要声明多个参数类型时,只需要使用逗号分隔参数列表即可。


public class Generic {
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());
}
}

这样在创建Generic实例时候,需要分别给出参数类型。

Generic generic = new Generic(“test”,123);

泛型类定语法:

class class-name{
//….
}

泛型类引用语法:

class-name var-name = new class-name(con-arg-list);

有界类型(bounded type)

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


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

Generic

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


class class-name

使用通配符参数

我们继续扩展上面这个类,当需要一个sameAvg()方法用来比较两个对象的average()接口是否相同,这个sameAvg()接口怎么写?
第一种方式:


boolean sameAvg(Generic ob){
if(average() == ob.average())
return true;
return false;
}

这种方式有一个弊端,就是Generic只能和Generic比较(上面说了),而我们比较相同平均数并care类型。这时我们可以使用通配符“?”来解决。
第二种方式:

boolean sameAvg(Generic ob){
//...
}

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

有界通配符

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




这样直有superclass类及其子类可以使用。也可以指定下界:


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

创建泛型方法

之前讨论泛型类中的泛型方法都是使用创建实例传递过来的类型,其实方法可以本身使用一个或多个类型参数的泛型方法。并且,可以在非泛型类中创建泛型方法。


class GenericDemo {
, 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;
}
}


, V extends T> boolean isIn(T x, V[] y)

泛型参数在返回类型之前,T扩展了类型Comparator,所以只有实现了Comparator接口的类才可以使用。同时V设置了T为上界,这样V必须是T或者其子类。通过强制参数,达到相互兼容。
调用isIn()时候一般可以直接使用,不需要指定类型参数,类型推断就可以自动完成。当然你也可以指定类型:

isIn(3,nums);

泛型方法语法:

ret-type meth-name(param-list){
//..
}

也可以为构造方法泛型化,即便类不是泛型类,但是构造方法是。所以在构造该实例时候需要根据泛型类型给出。

Generic(T a){
//..
}

泛型接口

泛型接口与定义泛型类是类似的


interface MyInterface>{
//...
}

当类实现接口时候,因为接口指定界限,所以实现类也需要指定相同的界限。并且接口一旦建立这个界限,那么在实现他的时候就不需要在指定了。

class MyClass> implements MyInterface{
//...
}

如果类实现了具体类型的泛型接口,实现类可以不指出泛型类型。

class MyClass implements MyInterface{
//...
}

使用泛型接口,可以针对不同类型数据进行实现;使用泛型接口也为实现类设置了类型限制条件。
定义泛型接口语法:

interface interface-name{
//...
}

实现泛型接口

class class-name implements interface-name{
//...
}

遗留代码中的原始类型

泛型是在JDK 5之后提供的,在JDK 5之前是不支持的泛型的。所以这些遗留代码即需要保留功能,又要和泛型兼容。可以使用混合编码,还比如上面的例子。Generic 类是一个泛型类,我们可以使用原始类型(不指定泛型类型),来创建Generic类。


Generic gen1 = new Generic(new Double(9.13));
double gen2 = (Double)gen1.getOb();

java是支持这种原始类型,然后通过强制类型转换使用的。但是正如我们上面说的,这就绕过了泛型的类型检查,它是类型不安全的,有可能导致运行时异常(RunTime Exception)。

泛型类层次

泛型类也可以是层次的一部分,就像非泛型类那样。泛型类可以作为超类或子类。泛型和非泛型的区别在于,泛型类层次中的所有子类会将类型向上传递给超类。


class Gen{
T ob;
Gen(T ob){
this.ob = ob;
}
T genOb(){
return ob;
}
}
class Gen2 extends Gen{
Gen2(T o){
super(o);//向上传递
}
}


Gen2 gen = new Gen2();

创建Gen2传入Integer类型,Integer类型也会传入超类Gen中。子类可以根据自己的需求,任意添加参数类型。

class Gen2 extends Gen{
Gen2(T a,V b){
super(a);//一定要有
}
}

超类也可以不是泛型,子类在继承的时候,就不需要有特殊的条件了。

class Gen{
Gen(int a){
}
}
class Gen2 extends Gen{
Gen2(T a,int b){
super(b);
}
Gen2(T a,V b,int c){
super(c);
}
}

需要注意的:

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

擦除

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

模糊性错误

泛型引入后,也增加了一种新类型错误-模糊性错误的可能,需要进行防范。当擦除导致两个看起来不同的泛型声明,在擦除之后可能变成相同类型,从而导致冲突。


class Gen{
T ob1;
V ob2;
void setOb(T ob){
this.ob1 = ob;
}
void setOb(V ob){
this.ob2 = ob;
}
}

这种是无法编译的,因为当擦除后可能会导致类型相同,这样的方法重载是不对的。

Gen gen = new Gen();

这样T和V都是String类型,明显代码是不对的。
可以通过指定一个类型边界,比如:

class Test1{
public static void main(String[] args){
//没问题
Gen gen = new Gen();
gen.setOb(1);
//这样在调用setOb的时候也会编译失败,因为都为Integer类型,方法重载错误
Gen gen1 = new Gen();
gen1.setOb(1);
}
}

所以在解决这种模糊错误时候,最好使用独立的方法名,而不是去重载。

使用泛型的限制

  • 不能实例化类型参数,因为编译器不知道创建哪种类型,T只是类型占位符。

    class Gen{
    T ob;
    Gen(){
    ob = new T();
    }
    }
  • 静态成员不能使用类中声明的类型参数。

    class Gen{
    //错误的,不能声明静态成员
    static T ob;
    //错误的,静态方法不能使用参数类型T
    static T getGen(){
    return ob;
    }
    //正确的,静态方法不是参数类型
    static void printXXX(){
    System.out.println();
    }
    }
  • 不能实例化类型参数数组

    //没问题
    T[] vals;
    //不能实例化类型参数数组
    vals = new T[10];
  • 不能创建特性类型的泛型应用数组

    //这是不允许的
    Gen gen = new Gen[10];
    但是可以使用通配符,并且比使用原始类型好,因为进行了类型检查。
    Gen gen = new Gen[10];
  • 泛型类不能扩展Throwable,这就意味着不嗯滚创建泛型异常类。

关注我

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


欢迎关注我

你可能感兴趣的:(深入理解Java泛型机制)