欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java泛型浅析

程序员文章站 2024-02-09 16:02:28
...

Java:泛型编程

​ 泛型类是具有一个或多个类型参数的类。类型参数用“<>”尖括号括起来写在类名之后,多个类型参数用“,”分隔。例如:

class Yeah<T> {
    ...
}

​ 类型参数一般是大写字母,而且很短,比如E,T,L这样的。Java标准库里就是用E来表示集合的元素类型,K,V表示表的关键字和值类型,T表示任意类型。

​ 声明了类型参数之后,就可以在其作用域内使用类型参数来声明变量类型或是作为返回类型。与泛型类类似的还有泛型接口的定义,可以说基本一致。

​ 类似地,还可以定义带有类型参数的方法。与定义泛型类时的区别在于,定义泛型方法时类型参数应该放在修饰符之后,返回类型之前。

​ 泛型方法的类型参数并不是依赖于类的,而是像泛型类那样,使用的时候传进去,换言之,泛型方法可以出现在一般类中,也可以出现在泛型类中。虽然一般来说,使用泛型方法需要传入类型参数,但如果声明的类型参数可以被推理出来,那么使用的时候也可省略。例如,假设声明了这样的泛型方法:

public static<T> T aMethod(T asshole){
	...
}
//按照规矩,应该这样调用该方法:
// Yeah.<String>aMethod("?");

​ 那么使用该方法时,若传入一个特定的参数对应aMethod中的参数“asshole”,编译器可以推理出这个参数的类型就是T,此时可以不传入类型参数。例如:

Yeah.aMethod("???");

​ 不过仅仅是孤零零的类型参数还不够。如果想要确保自己设计的泛型类/接口/方法只接纳实现了某些特定功能的类作为类型参数(例如,想要写一个泛型的对象比较方法,但是想要使用对象的compareTo方法),就不得不对类型参数加以约束。具体到实践中,使用诸如

class Nope<T extends Comparable>{
    ...
}

​ 的方法来限制类型参数必须实现了某些接口。注意,这里对接口并非是implements,而是extends。这表示泛型参数T是由那个“绑定类型”衍生而来,具有那个绑定类型的方法的同时可能还有一些扩增。一个类型参数可以有多个约束,彼此用 “&” 分割。另外,不仅是接口,也可以用类来作为约束,不过要注意两点:一是Java只允许单继承,故约束中至多存在一个类;二是如果约束列表里有类的话,必须把类放在约束列表的第一个。

了解泛型在运行时中的状态

​ Java虚拟机不认所谓的泛型。Java泛型在运行的过程中,应用了名为“类型擦除”的机制。

​ 无论何时定义一个泛型类型,都自动提供一个相应的原始类型,其名字跟原来定义的一样(只是少了类型参数),运行时将类型参数擦除,替换成约束列表中的第一个类型(若无,则为Object)。编写源程序时,可能用到多个不同类型参数的同名泛型类型,然而擦除之后就只剩下原始类型了。这一点也是Java泛型与C++模板的不同:C++模板的每种实例化都会产生新的类型,用的种类越多代码就越多(称为“模板代码膨胀”)而Java就不担心这个问题。

​ 如果程序的返回类型是类型参数,那么在编译成字节码时它就会被擦除,在调用方法的返回值处就有很大可能出现类型不匹配的情况。此时编译器自动在这里插入一个强制类型转换,转换成理应的类型。类似地如果类中有public的域,而程序中出现了对域的访问/修改,这时也会在该插入强制类型转换的地方插入强制类型转换。

泛型方法中的类型擦除

​ 泛型方法中的类型参数一样要被擦除。不过,对于泛型方法的类型擦除,还有一些特殊的地方。考虑如下的情况:

class SuperClass<T>
{
    private T data;
    public T getData()
    {
        return data;
    }
    public void setData(T data)
    {
        this.data = data;
    }
}
class DeriveClass extends SuperClass<Integer>
{
    @Override
    public Integer getData()
    {
        if(data < -114514)
        	return super.getData();
        else
            return 0;
    }
    @Override
    public void setData(Integer data)
    {
        System.out.print("I am son.");
        this.data = data;
    }
}

​ 如你所见,上面的代码包含两个类:泛型的父类SuperClass和继承Integer类型参数父类的子类DeriveClass,data是实例域,无论是父类方法还是子类方法都有getter和setter。

​ 我们先看setter:

​ 我们已经知道,编译成字节码时会擦除类型参数,在本例中是Integer被擦除为Object。此时子类应该看成

class DeriveClass extends SuperClass

​ 类型擦除后的SuperClass看起来是这样的:

class SuperClass
{
    private Object data;
    public Object getData()
    {
        return data;
    }
    public void setData(Object data)
    {
        this.data = data;
    }
}

​ 这下可糟了。我们的子类实现中可是要Override掉父类的setter的,可是类型擦除后,父类的setter跟子类的setter方法签名不同了,这样子类会有两个setter,一个参数类型是Object,一个是Integer。这不是我们想要的结果。而编译器解决这个问题的方法是使用桥方法

​ 所谓桥方法,只需要编译器在子类的定义中增加一个方法:

public void setData(Object data)
{
    public setData((Integer)data);
}

​ 这个方法把父类擦除后的方法覆盖,并且导向子类的“覆盖”方法。确实很像桥的作用。

​ 再看getter:

​ 我们已经知道父类类型擦除后的getter是

public Object getData()
    {
        return data;
    }

​ 大家都知道Java的方法签名是不包括返回值的,因此在覆盖父类方法时,可以将返回值类型变得更严格,称为可协变返回类型。然而实际上,编译器在子类的字节码中添加了这样的桥方法:

Object getData() //桥方法,导向子类的“覆盖”方法

​ 这样编写java字节码是不允许的,这和子类中的getter方法签名一致。不过,实际上JVM是通过方法签名+返回值类型来确定一个具体的方法的,故这种字节码形式是可以被JVM正确理解执行的。

泛型的局限性

​ 世上没有什么东西是完美的。

运行时类型检查

​ 程序员覆盖equals方法时常用的运算符instanceof,可以判断一个对象引用是否是给定的类型。不过,对于泛型类型的类型检查却不考虑类型参数。

if(x instanceof SuperClass<String>)
{
    ...
}
if(x instanceof SuperClass<T>)
{
    ...
}

​ 上面if括号内的运算符实际上可以理解为下面的。说白了,运行时类型检查只管你属不属于这个类,不管你属不属于这个类的某个特定类型。不过一般来说编译器根本不会让上面的代码通过。

​ 同样的道理也发生在强制类型转换上。将一个对象强制类转换成一个特定的泛型类型会弹出警告。

数组

​ 不能初始化一个有特定参数的泛型类型的数组。

SuperClass<String>[] ass = new SuperClass<String>()[233];//不行!

​ 这一点很好理解。如果这样做,由于类型擦除机制,ass在字节码中只会被当作SuperClass类型的数组,这样JVM对数组的保护只局限于是不是SuperClass类型,而对于SuperClass类型的不同类型参数类型则一律开绿灯。如果尝试把SuperClass<Integer>类型的变量赋给ass,可以通过检查,但是会导致类型错误。而这不是程序员希望看到的。因此编译器会禁止这样的做法。

​ 虽然不允许初始化这样的数组,但是声明这样的一个数组引用还是可以的。

​ 要达到类似效果的替代,一种方法是初始化一个通配符类型的数组,比如SuperClass<?>()[233],然后在使用时进行类型转换。但是这样做非常不安全。

​ 更安全的替代是使用Arraylist<SuperClass<String>>。Arraylist真的很强大!

Varargs(可变参数)警告

​ 跟泛型在数组中的局限性密切相关。如果你了解过可变参数个数的方法的写法,你该知道如果尝试声明一个可变类型的参数实际上会传给方法一个对应类型的数组。考虑下面的情况:

public <T> void aStupidMethod(T... manyT)
{
    for(T t:manT)
        ...
}

​ Oops!manyT岂不是会被JVM创建为一个数组?不过根据上面所说,这样做有大风险。谁知道会不会有人特地在调用这个方法时传入一堆奇奇怪怪的参数?不过编译器对这种情况只会提示警告。可以使用@suppressWarnings(“unchecked”)或者@SafeVarargs来抑制此警告。

尝试实例化类型变量

​ 不能直接使用new关键字实例化一个类型变量的对象。类型擦除之后,岂不是变成了new Object(…)?这肯定不是想要达到的效果。不过,想在泛型类中实例化类型参数的对象,可以利用反射机制。

类型参数数组

​ 不能在泛型类中实例化一个类型参数的数组。数组也是有类型的,虚拟机时刻监控着数组的类型和其中元素的类型。在类型擦除的过程中,类型参数的数组也要受到影响。

不能在泛型类的静态上下文中引用类型变量

​ 在静态域、方法中引用类型变量,都是不行的。不能声明一个这样的域:

private T sillyDomain;

​ 方法也是同理。

不能为泛型类填充Throwable

​ 不可以为泛型类扩展Throwable及其子类。这样也就不能抛出/捕获泛型Throwable类型。