<数据结构与算法分析>读书笔记--利用Java5泛型实现泛型构件

时间:2022-02-25 10:17:20

一、简单的泛型类和接口

当指定一个泛型类时,类的声明则包括一个或多个类型参数,这些参数被放入在类名后面的一对尖括号内。

示例一:

package cn.generic.example;

public class GenericMemoryCell <AnyType>{

    public AnyType read() {

        return storedValue;
} public void write(AnyType x) { storedValue=x;
} private AnyType storedValue; }

GenericMemoryCell有一个类型参数。在这个例子中对类型参数没有明显的限制,所以用户可以创建像GenericMemoryCell<String>和GenericMemoryCell<Integer>类声明内部,我们可以声明泛型类型的域和使用泛型类型作为参数或返回类型的方法。比如,类GenericMemoryCell<String>的write方法需要一个String类型的参数。如果传递其它参数那将产生一个编译错误。

同时也可以声明接口是泛型的。

示例二:

package cn.generic.example;

public interface Comparable <AnyType>{

    public int compareTo(AnyType other);

}

在Java5以前,Comparable接口不是泛型,而它的comparaTo()方法需要一个Object作为参数。于是,传递到compareTo方法的任何引用变量即使不是一个合理的类型也都会编译,而只是在运行时报告ClassCastException错误。在Java5中Comparable接口是泛型的。

再比如以我目前用到的ORM框架MyBatis-Plus,其中的BaseMapper也是泛型接口,如下图所示:

<数据结构与算法分析>读书笔记--利用Java5泛型实现泛型构件

二、自动装箱和拆箱

什么是装箱和拆箱?

一句话概括:装箱就是自动将基本数据类型转换为包装器类型;拆箱就是  自动将包装器类型转换为基本数据类型。

示例三(可与<数据结构与算法分析>读书笔记--实现泛型构件pre-Java5 中的示例三代码进行比较):

package cn.generic.example;

public 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);
}
}

三、菱形运算符

以上面的示例三代码中的GenericMemoryCell<Integer> m = new GenericMemoryCell<Integer>()来说,有些烦人,因为既然m是GenericMemoryCell<Integer>类型的,显然创建的对象也必须是GenericMemoryCell<Integer>类型的,任何其他类型的参数都会产生编译错误。Java7增加了一种新的语言特征,称为菱形运算符。

可以将GenericMemoryCell<Integer> m = new GenericMemoryCell<Integer>()改写为GenericMemoryCell<Integer> m = new GenericMemoryCell<>()

示例四:

package cn.generic.example;

public class BoxingDemo {

    public static void main(String[] args) {

        GenericMemoryCell<Integer> m = new GenericMemoryCell<>();
m.write(5);
int val = m.read();
System.out.println("Contents are:"+val);
}
}

四、带有限制的通配符

带限制的通配符,通常有两种表现形式:

  • ? extends E

  • ? super E

使用原则可遵循PECS原则,其实就是四个单词的组合。

PECS — producer-extends, consumer-super

翻译过来就是生产者继承,消费者使用。

五、泛型static方法

有时候特定类型很重要,或许是因为下面几个原因:

(1)特定类型用作返回类型;

(2)该类型用在多于一个的参数类型中;

(3)该类型用于声明一个局部变量。

如果是这样,那么,必须要声明一种带很多类型参数的显式泛型方法。

示例五:

package cn.generic.example;

public class GenericStaticExample {

  public static <AnyType> boolean contains(AnyType[]arr, AnyType x) {

     for(AnyType val:arr)

             if(x.equals(val)) 

                 return true;

         return false;

  }

}

上面显示是一种泛型static方法,该方法对值x在数组arr中进行一系列查找。通过使用一种泛型方法,代替使用Object作为参数的非泛型方法,当在Shape对象的数组中查找Apple对象时我们能够得到编译时错误。

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

六、类型限界

示例六(在一个数组中找出最大元的泛型static方法,以例说明类型参数的限界)

package cn.generic.example;

public class TypeLimitExample {

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

          int maxIndex = 0;

          for (int i = 0; i < arr.length; i++) 

              if(arr[i].compareTo(arr[maxIndex])>0)
maxIndex = i; return arr[maxIndex]; } }

七、类型擦除

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

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

八、对于泛型的限制

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

1.基本类型

基本类型不能用做类型参数。因此,GenericMemoryCell<int>是非法的。我们必须要使用包装类。

2.instanceof检测

instanceof检测和类型转换工作只对原始类型进行。

示例七:

package cn.generic.example;

public class InstanceOfCheckExample {

    public static void main(String[] args) {
GenericMemoryCell<Integer> celll = new GenericMemoryCell<>();
celll.write(4); Object cell = celll; GenericMemoryCell<String> cell2 = (GenericMemoryCell<String>) cell;
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<>[10];
GenericMemoryCell<Double> cell = new GenericMemoryCell<>();
cell.write(4.5);
Object[] arr2 = arr1;
arr2[0] = cell;
String s = arr1[0].read();

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

示例代码已经上传到我的Github:https://github.com/youcong1996/The-Data-structures-and-algorithms/tree/master/Introduction