Java动态编程之泛型

1. 泛型的概念

泛型本质上就是参数化类型(parameterized type)。在定义类、接口、方法的时候,把将要操作的数据类型声明为形参。在实例化的时候,再传入实际的数据类型,就是由类型实参指定真实数据类型。这就是泛型。泛型主要目的是复用算法。对于不同的数据类型,算法逻辑一样,就不用针对不同数据类型写不同的代码。可以增强代码的可扩展性,减少工作量。比如ArrayList,可以保存任何数据类型,不需要针对每个数据类型写一个ArrayList类。

在Java 5之前,没有泛型。在这之前,复用算法的主要手段是使用超类。比如可以将ArrayList里面的数据类型定义为Object,这样就可以保存任何数据类型了。但是,这样容许使用者在同一个ArrayList保存不同的数据类型,在代码进行强制类型转换的时候,就会发生类型错误。泛型可以保证类型安全,因为它可以让强制类型转换自动地、隐式地进行。

但是,因为Java 5之前没有泛型,所以泛型需要兼容以前非泛型的代码。就是非泛型代码必须能处理泛型,泛型代码也必须能处理非泛型代码。Java是使用擦除特性(erasure)来实现的。当Java代码编译时,所有泛型类型信息都将被删除(擦除)。也就是说,当java文件编译成class文件后,会使用类型形参的约束类型来替换类型形参。举个例子,ArrayList<E>中保存的元素,在class文件看来就是Object。然后使用隐式地强制转换来与类型实参指定的类型保持兼容。

总结一下就是:泛型扩展了复用算法的能力,并且可以保证类型安全。通过编译后类型擦除和隐式强制转换,保证和以前的非泛型代码的兼容性。

2. 对类型形参进行约束

有些情况下,泛型类型可以没任何约束,也就是任何Object都可以。但是在另外一些情况下,算法只和特定的数据类型有关。比如你希望对任何数值(包括整数,浮点数和双精度数)执行计算。这时候需要用到Number类中的一些方法。如果没有进行约束,泛型类型是不能调用Number类中的方法的。下面是对类型形参进行约束的代码示例:

错误的写法:

class NumericFns<T> {
    T num;

    NumericFns(T t) {
        num = t;
    }

    double getDoubleValue() {
        //这是错误的写法,因为编译后num被当做Object类型,Object类型没有doubleValue()方法。
        return num.doubleValue();
    }
}

正确的写法:

class NumericFns<T extends Number> {
    T num;

    NumericFns(T t) {
        num = t;
    }

    double getDoubleValue() {
        //这是正确的写法,因为编译后num被当做Number类型,Number类型有doubleValue()方法。
        return num.doubleValue();
    }
}

上面这个例子类型形参T被限定为Number类的子类。

另外,一个形参可以用来约束另一个形参:

class Pair<T,V extends T> {
    T first;
    V second;
    Pair(T a,V b) {
        first = a;
        second = b;
    }
}

上面这个例子中,T可以是任何类型,但是一旦指定了T,V就必须是T的子类型。下面这段使用代码就是错的,因为String不是Number的子类:

Pair<Number,String> z = new Pair<Number,String>(10,"10");

3. 使用通配符实参以及约束通配符

如果要写一个算法,要对一个浮点数的绝对值和一个双精度数的绝对值进行比较,该怎么办呢?看下面的代码示例:

class NumericFns<T extends Number> {
    T num;

    NumericFns(T t) {
        num = t;
    }

    boolean absEqual(NumericFns<?> ob) {
        if(Math.abs(num.doubleValue())==Math.abs(ob.num.doubleValue())) {
            return true;
        }
        return false;
    }
}

根据上面的代码,我们可以写出使用代码:

NumericFns<Float> f = new NumericFns<Float>(1.25f);
NumericFns<Double> d = new NumericFns<Double>(-1.25);
f.absEqual(d);

注意,代码中?代表的是实参,表示该方法可以传入任何Number类型的实参。这样就达到了让不同类型数据进行比较的目的。通配符实参在不确定实参类型的场景下非常实用。

另外,也可以对通配符进行约束:

//上层约束
<? extends superclass>
//下层约束
<? super subclass>

4. 使用泛型需要注意的地方

4.1 泛型的作用范围

① 泛型的形参可以在类、接口、构造函数、方法(包括成员方法和静态方法)中声明。

② 类和接口中声明的形参可以使用在:成员变量、成员方法参数、成员方法返回类型。不能用在静态变量和静态方法上(虽然静态方法可以定义自己的形参,但是不能用类和接口中声明的形参,因为类和接口中声明的形参是给对象用的,而不是给类用的)。

class NumericFns<T extends Number> {
    //这是对的
    T num;
    //这是错的
    static T num1;

    NumericFns(T t) {
        num = t;
    }

    //这是对的
    T getNum() {
        return num;
    }

    //这是错的
    static T getNum1() {
        return num;
    }

    //这是对的
    static <V extends Number> V getNum2(V v) {
        return v;
    }
}

③ 泛型的形参不可以是基本数据类型。

4.2 类型形参不能实例化

下面这段代码是错误的,因为编译器不知道要创建哪一种对象,T只是一个占位符:

class Gen<T> {
    T ob;
    T vals[];
    Gen() {
        //这是错的
        ob = new T();
        //这也是错的
        vals = new T[10];
    }
}

对于数据,还有一个限制:

//这是错误的
Gen<Integer> gens[] = new Gen<Integer>[10];
//这是对的
Gen gens[] = new Gen[10];

4.3 歧义错误

下面这两个set方法就会产生冲突,如果T和V都传入String实参,就会产生歧义,这是编译不能通过的。

class MyGenClass<T,V> {

    T ob1;
    V ob2;
    void set(T o) {
         ob1 = o;
    }
    void set(V o) {
         ob2 = o;
    }

}

4.4 实现了泛型接口的类,其自身也必须是泛型的

4.5 方法中声明的形参,一定要用在方法参数中

4.6 对于形参的声明都放在<>里面

4.7 扩展了Throwable的类都不能使用泛型,也就是说不支持泛型异常类

5. 反射和泛型

从Java 5以后,Class类是泛型的。String.class实际上是Class<String>的一个实例(唯一的实例)。

public static <T> Pair<T> makePair(Class<T> c)
    throws InstantiationException,IllegalAccessException {

    return new Pair<>(c.newInstance(),c.newInstance());

}

makePair方法如果传入一个String.class,就会返回一个Pair<String>实例。这在构建框架代码时,非常常见。

反射是一个非常大的动态编程主题,后面会专门讨论它。

 

转载于:https://my.oschina.net/leaforbook/blog/1842251

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值