利用Java5泛型实现泛型特性成分

本文参考节选自  机工社《数据结构与算法分析 Java语言描述》

 

 

一. 简单的泛型类和接口

 

首先看以下代码中的GenericMemoryCell泛型类:

public class GenericMemoryCell<AnyType>
{
   public AnyType Read(){return storedValue;}
   pubic void Write(Anytype x){storedValue = x;}
   private AnyType storedValue;
}

当声明一个泛型类时,类的声明则包含一个或者多个类型参数,这些参数被放在类名后面的一对的尖括号内。代码中的第一行指出了GenericMemoryCell拥有一个类型参数。在这个例子中对类型参数没有明显的限制,所以用户可以创建像GenericMemoryCell<String>GenericMemoryCell<Integer>这样的类型但是不能创建GenericMemoryCell<int>这样的类型GenericMemoryCell类声明内部,我们可以声明泛型的类型的域和使用泛型类型作为参数或者返回类型的方法。例如以上代码的第5行GenericMemoryCell<String>的Write方法需要一个String类型的参数。如果传递其他参数将会产生一个编译错误。也可以声明接口为泛型的。例如,在Java5以前Comparable接口不是泛型的,而它的compareTo方法需要一个Object类型作为参数。于是,传递到compareTo方法的任何引用变量即使不是一个合理的类型也会进行编译,而只是在运行时报告ClassCastException异常。在Java5中,Comparable接口是泛型,如下代码所示:

public interface Comparable<AnyType>
{
      public int compareTo(AnyType other);
}

现在String类实现了Comparable<String>并有一个compareTo方法,这个方法一个String作为其参数。通过使类变成泛型类,以前只有在运行时才报告的异常,现在在编译时就能报异常。

 

二. 自动装箱和拆箱

 

首先看以下代码:

public Class WrapperDemo
{
  public static void main(String []args)
  {
    MemoryCell m = new MemoryCell();
    m.write(new Integer(37));
    Integer wrapperVal = (Integer)m.read();
    int val = wrapperVal.intvalue();
    System.out.println("Contents are:"+val);
  }
}

上面的代码写的很麻烦,因为使用包装类需要在调用write之前创建Integer对象,然后才能使用inValue方法从Integer中提取int值。在Java5之前这是需要的,因为一个int类型的量被放到需要Integer类型的地方,那么编译将会产生一个错误信息,而如果将一个Integer对象的结果赋给一个int型的量,则编译也将产生一个错误信息。所以说,上面的代码准确的反映了基本类型和引用类型之间的区别,但是还没有清楚地表示出程序员把那些int存入集合(collection)的意图。

而Java5 矫正了这种情况,如果一个int类型被传递到一个需要Integer类型的地方,那么编译器将在幕后插入一个对Integer 构造方法的调用。这就是自动装箱。而如果一个Integer被放到一个需要int类型的地方,则编译器将在幕后插入一个对intValue方法的调用,这就叫自动拆箱。对于其他的7种基本类型,同样会发生类似的情形。下面的代码描述了自动装箱和自动拆箱的使用。注意:在GenericMemoryCell中引用的那些实体依旧是Integer对象;在GenericMemoryCell实例化过程中int不能够替代Integer。

Class BoxingDemo
{
    public static void main(String []args)
    {
       GenericMemoryCell<Integer> m = new GenericMemoryCell<Integer>();
       m.write(37);
       int val = m.read();
       System.out.println("Contents are:"+val);
    }
}

 

三. 带有限制的通配符

 

首先看以下代码:

public static double totalArea(Shape []arr)
{
   double total =0;
   for(shape s: arr)
     if(s! = null)
       total += s.area();
   return total;
}

这是一个static方法,该方法计算了一个shape数组的总面积(假设Shape是含有area方法的类;而Circle和Square是继承Shape的类)。假设我们要重写这个计算总面积的方法,使得该方法能够使用Collection<Shape>这样的参数。Collection在此不进行讨论,当前唯一重要的是它能够存储一些项,而这些项能够用一个增强的for循环来处理。由于是增强的for循环,因此代码是相同的,如下代码所示:

public static double totalArea(Collection<Shape> arr)
{
   double total =0;
   for(shape s: arr)
     if(s! = null)
       total += s.area();
   return total;
}

如果传递一个Collection<Shape>,那么程序会正常运行。但是如果传递一个Collection<Square>会发生什么情况呢?答案依赖于是否Collection<Square> IS-A Collection<Shape>。在Java中数组是协变的,于是Square[ ] IS-a Shape[ ]。一方面,这种一致性意味着数组是协变的,那么集合也是协变的。而另一方面,我们可以知道数组的协变性导致代码得以编译,但是在运行时会抛出一个ArrayStoreException运行时异常。

因为使用泛型的全部原因就在于为了产生编译错误而不是产生运行不匹配的运行时异常,所以泛型集合不是协变的。因此我们不能把Collection<Square>作为参数传递到totalArea方法中去。

于是现在我们又遇到了新的问题,泛型(及泛型集合)由于不是协变的,而数组是协变的。若没有附加的语法,那么用户就会避免使用集合(Collection),因为失去协变性让代码缺少灵活性。

Java5使用通配符来弥补了这个不足。通配符用来表示参数类型的子类或者超类,以下代码描述了带有限制的通配符的使用:

public static double totalArea(Collection<? extends Shape> arr)
{
   double total =0;
   for(shape s: arr)
     if(s! = null)
       total += s.area();
   return total;
}

其中编写一个将Collection<T>作为参数的方法totalArea,其中的T  IS-a Shape。因此,Collection<Shape>和Collection<Square>都是可以接受的参数。通配符还可以不受限制使用(此时假设extends Object),或者不用extends而是用super(来表示超类而不是子类)。

 

四. 泛型的static方法

 

继续首先看下面的代码:

public static double totalArea(Collection<? extends Shape> arr)
{
   double total =0;
   for(shape s: arr)
     if(s! = null)
       total += s.area();
   return total;
}

从某种意义上来说,上面的代码中的totalArea方法是一个泛型方法,因为它能够接受不同类型的参数。但是这里没有特定类型的参数表,正如在GenericMemoryCell类的声明中所做的那样,有时候特定类型很重要,这或许是由于以下的原因:

  • 该特定类型用作返回类型
  • 该类型用在多于一个的参数类型中
  • 该类型用于声明一个局部变量

如果是在这样,那么必须要声明一种带有若干参数的的显式泛型方法。

例如下列代码就描述了一种泛型的static方法,该方法对x在数组中进行一系列的查找。通过使用一种泛型方法,代替使用Object对象作为参数的非泛型方法,当在Shape对象的数组中查找Apple对象时我们得到编译时错误。

public static <AnyType> boolean contains(AnyType []arr,AnyType x)
{
    for(AnyType val:arr) 
       if(x.equals(val))
          return true;
    return false;
}

泛型方法特别像泛型类,因为类型参数表使用相同的语法,在泛型方法中类型参数表位于返回类型之前。

 

五. 类型界限

 

假设我们想要编写一个findMax方法。先看以下的代码:

public static <AnyType> AnyType findMax(AnyType[] arr)
{
     int maxIndex = 0;
     for(int i=1;i<arr.length;i++)
        if(arr[i].compareTo(arr[maxIndex])>0)
           maxIndex = i;
     return arr[maxIndex];
}

由于在上面的代码第6行编译器不能保证对于compareTo的调用时合法的,因此程序不能正常运行;只有在AnyType是Comparable的情况下才能保证compareTo的存在。不过我们可以使用类型限界来解决这个问题。类型限界在简括号内进行指定,它指定参数类型必须具有的特性。所以一种自然而然的想法是把性质改写成:

public static <AnyType extends Comparable> AnyType findMax(AnyType []arr)
{...}

我们知道如今在Java5下Comparable接口如今是泛型的,所以这种做法非常自然而然,不过更好的方法应该如下:

public static <AnyType extends Comparable<AnyType>> AnyType findMax(AnyType []arr)
{...}

然而这个方法仍然不能让人满意。为了看清这个问题,按以下思路再思考这个问题:

首先假设Shape类实现接口Comparable<Shape>,再假设Square类继承Shape类,至此我们可知Square实现接口Comparable<Shape>。于是Square IS-a Comparable<Shape>,但是Square IS-NOT-a Comparable<Shape>!!!

应该说,AnyType IS-a Comparable<T>, 其中T是AnyType的父类由于我们不知道准确的父类型,因此可以使用通配符。那么最后的结果变为:

public static <AnyType extends Comparable<? super AnyType>> AnyType findMax(AnyType []arr)

{...}

以下的代码为findMax方法的实现:

public static <AnyType extends Comparable<? super AnyType>> AnyType findMax(AnyType []arr)
{
     int maxIndex = 0;
     for(int i=1;i<arr.length;i++)
        if(arr[i].compareTo(arr[maxIndex])>0)
           maxIndex = i;
     return arr[maxIndex];
}

对于以上的代码,编译器接受了类型T的数组,只是使得T实现Compare<S>接口,其中T IS-A  S。

 

六. 类型擦除

 

泛型很大程度上是Java 语言中的成分而不是虚拟机中的结构。泛型类可以由所谓的类型擦除(type erasure)过程而转变成非泛型类。这样编译器就生成一种和泛型类同名的原始类(raw class),但是类型参数都被删去了。类型变量由它们的类型限界来代替,当一个具有擦除返回类型的泛型方法被调用时候,一些特性被自动的插入。如果使用一个泛型类而不带类型参数,那么使用的是原始类。

类型擦除的一个很重要的推论是:所生成的代码和程序员在泛型之前所写的代码并没有太大的不同,而且事实上运行的也并不快。不过其显著的优点在于,程序员不必把一些类型转化放到代码当中,编译器将进行重要的类型检验。

 

七. 对于泛型的限制

 

对于泛型类型有许多的限制。由于类型擦除的原因,这里列出的每一个限制都是必须要遵守的。

 

(1)基本类型

基本类型不能用作类型参数

 

(2)instanceof检测

instanceof 的类型检测和转换工作只针对原始类型进行。例如以下的代码:

GenericMemoryCell<Integer> cell1= new GenericMemoryCell<Integer>();
cell1.write(4);
Object cell = cell1;
GenericMemoryCell<String> cell2= new GenericMemoryCell<String>();
String s = cell2.read();

这里的运行转换在运行时是成功的,因为所有的类型都是GenericMemoryCell。但是在最后一行,由于对read的调用企图返回一个String对象从而产生一个运行时异常。结果类型转换将产生一个警告,而对应的instanceof 检测是非法的。

 

(3)static的语境

在一个泛型类中,static类和static域都不可引用类的类型变量,因为在类型擦除后类型变量就不复存在了。另外由于只存在一个原始的类,因此static域在该类型的诸泛型实例之间是共享的。

 

(4)泛型类型的实例化

不能创建一个泛型类型的实例。如果T是一个类型变量,那么语句:

T obj = new T();

是非法的。T由它的限界代替,这可能是Object类甚至是抽象类,因此对new的调用没有意义。

 

(5)泛型数组对象

也不能创建一个泛型数组,如果T是一个类型变量,则语句:

T[] arr = new T[10];

是非法的。T将有它的限界代替,这很有可能是Object T,于是(有擦除类型产生的)对T[ ] 的类型转换将无法进行,因为Object IS-NOT-A T[ ]。由于我们不能创建泛型对象的数组,一般来说我们必须创建一个擦除类型的数组,然后使用类型转换。这种类型转换将产生一个为检验的类型转换的编译警告。

 

(6)参数化类型数组
参数化类型的数组的实例化是非法的。考虑一下代码:

GenericMemoryCell<String> []arr1 = new GenericMemoryCell<String>[10];
GenericMemoryCell<Double> cell = new GenericMemoryCell<Double>();cell.write(4.5);
Object []arr2 = arr1;
arr2[0] = cell;
String s arr1[0].read();

正常情况下,我们认为i第4行的赋值会生成一个ArrayStoreException,这是因为赋值类型有错。可是,在擦除类型后,数组类型为GenericMemoryCell[ ],而加到数组中的对象也是GenericMemoryCell,因此不存在ArrayStoreException异常。于是代码没有类型转换,它最终将在第5行代码产生一个ClassCastException异常,这正是泛型应该避免的情况。



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值