泛型

Kiyotaka Wang Lv3

泛型

最近在看《数据结构与算法分析——Java语言描述》的时候接触到了泛型这个概念。虽然大一下学期软工一大作业也碰到过泛型,但基本上都是对着助教给的参考框架抄的,并没有深入学习。恰逢最近C++也碰到了模板Template的概念,机不可失,在此稍微学习一下。

首先,面向对象的一个重要目标是对代码重用的支持。支持这个目标的一个重要的机制就是泛型机制(generic mechanism):如果除去对象的基本类型外,实现方法是相同的,那么我们就可以用泛型实现(generic implementation)来描述这种基本的功能。例如,可以编写一个方法,将由一些项的数组排序;方法的逻辑关系与被排序的对象的类型无关,此时可以使用泛型方法。

使用Object表示泛型

Java的基本思想就是可以使用像Object这样适当的超类来实现泛型类。

// MemoryCell class
// Object read()
// void write(Object x)
public class MemoryCell {
    private Object storedValue;
    
    public Object read(){
        return storedValue;
    }
    public void write(Object x){
        storedValue = x;
    }
}

需要注意的是,这种写法涉及到了强制类型转换,有些时候会造成不必要的麻烦。此外,第二个重要的细节是不能使用基本类型,因为只有引用类型能够与Object相容。

基本类型的包装

当我们实现算法的时候,常常遇到语言定型问题:我们已经有一种类型的对象,可是语言的语法却需要一种不同类型的对象。

这种技巧阐释了包装类(wrapper class)的基本主题。一种典型的用法是存储一个基本的类型,并添加一些这种基本类型不支持或不能正确支持的操作。

在Java中我们已经看到,虽然每一个引用类型和Object相容,但是,8种基本类型却不能。于是,Java为这8种基本类型中的每一种都提供了一个包装类。例如,int类型的包装是Integer。每一个包装对象都是不可变的(就是说它的状态绝不能改变),它存储一种当该对象被构建时所设置的原值,并提供一种方法以重新得到该值。包装类也包含不少的静态实用方法。

PS: Java17已经支持了基本类型自动转为相应的包装类。因此省去了不少的麻烦。这本书还是比较老的,其版本还停留在Java5,实操下来发现不少麻烦的事情已经被解决了。

public class WrapperDemo {
    public static void main(String[] args) {
        MemoryCell m = new MemoryCell();

        m.write(37);
        int val = (Integer) m.read();
        System.out.println("Contents are: " + val);
    }
}

简单的泛型类和接口

public class GenericMemoryCell<T>{
    private T storedValue;
    public T read(){
        return storedValue;
    }
    public void write(T x){
        storedValue = x;
    }
}

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

也可以声明接口是泛型的,比如Comparable就是一个泛型接口。

带有限制的通配符

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

与数组不同,泛型集合不是协变的,因此Java 5用通配符(wildcard)来弥补这个不足。通配符用来表示参数类型的子类(或超类)。上述例子中Collection<T>的T IS_A Shape。因此Collection<Shape>和Collection<Square>都是可以接受的参数。通配符还可以不带限制使用(此时假设extends Object),或不用extends而用super(来表示超类而不是子类)。

泛型static方法

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

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

  1. 该特定类型用做返回类型
  2. 该类型用在多于一个的参数类型中
  3. 该类型用于声明一个局部变量

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

类型限界

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存在。我们可以使用类型限界(type bound)解决这个问题。

一种朴素的想法是

public static <Anytype extends Comparable>...

我们知道,因为Comparable接口如今是泛型的,所以这种做法很自然。虽然这个程序能够被编译,但是更好的做法确实

public static <AnyType extends Comparable<AnyType>>...

然而,这个做法还是不能令人满意。为了看清这个问题,假设Shape实现Comparable<Shape>,设Square继承Shape。此时,我们所知道的只是Square实现Comparable<Square>。于是,Square IS_A Comparable<Shape>,但它 IS_NOT_A Comparable<Square>!

应该说,AnyType IS_A Comparable<T>,其中,T是一个AnyType的父类。由于我们不需要知道准确的类型T,因此可以使用通配符。最后的结果变成

public static <AnyType extends Comparable<? super AnyType>>

下面是一个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实现Comparable<S>接口,其中T IS_A S。当然限界声明看起来有些混乱。幸运的是,我们不会再看到任何比这种用于更复杂的用语了。

PS:

值得注意的是,Comparable是一个接口,但是我们却用extends来表示对其的继承,而不是implements来表示实现

就结论而言:

对泛型来说,extends这个关键词代表“是一个… …”,且适用于类和接口;

对类的扩展来说 ,extends只适用于对父类的继承,implements只适用于对接口的实现。

注意: 泛型限定中extends绑定的是限定类型及其子类 泛型限定中super绑定的是限定类型及其父类

参考了csdn的一篇博客https://blog.csdn.net/Mrsgflmx/article/details/107819929,详情可以见《Head First Java》,南软的软工I学的太浅了,说实话很多Java的知识至今还没掌握,看到这个extends的时候我也是挺懵逼的,看来有空还是得继续读一读《On Java》

对于泛型的限制

1. 基本类型

基本类型不能做类型参数。必须使用包装类。

2. instanceof检测

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

public class GenericMemoryCell<T>{
    private T storedValue;
    public T read(){
        return storedValue;
    }
    public void write(T x){
        storedValue = x;
    }

    public static void main(String[] args) {
        GenericMemoryCell<Integer> cell1 = new GenericMemoryCell<>();
        cell1.write(4);
        Object cell = cell1;
        GenericMemoryCell<String> cell2 = (GenericMemoryCell<String>) cell;
        String s = cell2.read();
    }
}

会产生下面的异常

Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap') at GenericMemoryCell.main(GenericMemoryCell.java:15)

这里的类型转换在运行时是成功的,因为所有的类型都是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];

是非法的。

6. 参数化类型的数组

参数化类型的数组的实例化是非法的。感觉书写的云里雾里的,可以看看这篇博客。

(53条消息) 不能创建参数化的泛型数组_折耳狐的博客-CSDN博客_不能创建参数化类型的数组

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

如果第一行new GenericMemoryCell<>就是会报错的,但这样子写似乎就没有问题了。我用的是jdk17,所以相比较原书的版本有了很大的变化,因此按照编译器的提示,修改下来,其实也是可以运行的。但总之,参数化类型的数组的实例化是一件非常危险的事情,还是尽量不要干比较好。

总结

其实还是我才疏学浅,并没有怎么用过泛型,唯一一次还是软工大作业助教的模板里面见到的,总的来说还是有许多不明白的,等以后遇到了,被坑到了再更新吧。

  • 标题: 泛型
  • 作者: Kiyotaka Wang
  • 创建于 : 2022-10-03 10:36:32
  • 更新于 : 2022-11-21 12:59:25
  • 链接: https://hmwang2002.github.io/2022/10/03/fan-xing/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。