泛型
泛型
最近在看《数据结构与算法分析——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类的声明中所做的那样。有时候特定类型很重要,这或许因为下列的原因:
- 该特定类型用做返回类型
- 该类型用在多于一个的参数类型中
- 该类型用于声明一个局部变量
如果是这样,那么,必须要声明一种带有若干类型参数的显示泛型方法。例子如上。
类型限界
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. 参数化类型的数组
参数化类型的数组的实例化是非法的。感觉书写的云里雾里的,可以看看这篇博客。
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 进行许可。