概要
这一章的复习主要涵盖 泛型推断 ,协变和逆变,泛型擦除,泛型的工作原理,泛型通配符(有界和无界通配符),泛型容器写法,泛型边界,自限定泛型 和 潜在类型机制补偿。这里整理的都是个人觉得比较重要重新认真分析的东西,不一定全部为基础,如果有不准确的地方,还望大佬从评论区指出,感激不尽。
文章中的所有的自定义的测试都在自己的 java-review
仓库中,本文对应的链接:
泛型概念
- 泛型实现了参数化类型的概念
- 泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,在运行时不存在任何类型相关的信息
- 泛型的类型检查出现在 静态类型检查期,编译时会将所有的泛型擦除
- 基本类型无法作为泛型的类型参数,但是自动装箱机制使得基本类型能够自动转换为对应的包装类型传入
- 数组并不支持泛型,无法创建泛型数组
泛型的使用
类继承中的泛型
泛型类的泛型声明在 类名后面
1 | // test |
类继承中的泛型。我自己理解为: 子类相当于 使用指定泛型类型 的父类型
- 指定类型。
extends
父类SuperClass<T, E>
,将父类声明为SuperClass<String, Integer>;
直接指定所有泛型 或者SuperClass<T, Integer>
指定部分泛型。 - 这种情况的时候,子类中必 须把父类声明中没有的指定的泛型参数同时声明
SubClass<T> extends SuperClass<String, T>
,才不会报错
泛型方法
泛型方法可以独立存在不依赖于泛型类,方法的泛型声明在方法 返回值之前
1 | // test |
静态方法不能使用类声明的泛型参数,这里的性质可以看出,泛型类似于非静态成员变量,依赖于实例化对象,而且只有的在类初始化的时候指定泛型类型才会生效,依托于对象实例,这个性质在下面类型推断中的 泛型的显示说明 中有同样的体现
泛型推断
调用泛型方法调用会根据 接收类型的泛型类型 自动地进行泛型推断,从而确定泛型方法中应该使用的 正确的泛型类型
适用的场景
- 赋值操作
- 直接传给形参
1 | // test |
显式指定调用方法的泛型
- 非静态方法。使用
this
关键字来调用方法,方法前面使用泛型this.<Generic Type>method()
- 静态方法。使用
ClassName.<Generic Type>method()
显式说明泛型类型
泛型原理
泛型动作都发生在边界,对于一个方法就像是进入方法和出方法
set(T t)
会进行静态编译检查T get()
会给传递出去的值插入转型
1 | // test |
checkcast
是进行类型转化的字节码指令,将 Object
转化为 Integer
泛型擦除
泛型只出现在 静态类型检查期,在运行的时候都会被擦除,所以无法获取到泛型参数的类型信息
1 | // test |
泛型容器
不能通过泛型直接创建 泛型对象 或者 泛型数组,所以创建一个泛型数组的方法,可以通过两种方法
Object[] arr
保存,获取数组元素的时候进行强制类型转化return (T)arr[index];
T[] arr = (T[])new Object[len];
使用泛型数组引用保存,初始化时直接进行类型转化
两种方式都 <font 无法通过类型转化获得泛型的数组,这和数组的本身性质有关
- 强制类型转化无法改变数组的底层类型,从
Integer[]
转换到了Number[]
其底层类型依旧是Integer[]
类型
泛型容器的写法
1 | // test |
- 强制类型转化并 不会影响数组的实际类型,数组的实际类型在创建的时候就已经确定了
- 强制类型转化 只能向上转型,或者向其实际类型的类型进行窄化向下转型(例如: 已经转化为了
Object[]
的),此外的转型都会抛出ClassCastException
类型转化异常
协变和逆变
逆变与协变用来描述类型转换后的继承关系,其定义:
如果
A
、B
表示类型,f(⋅)
表示类型转换,<=
表示继承关系 (比如:A <= B
表示A
是由B
派生出来的子类)
1 | f(⋅) 是逆变的,当 A <= B 时有 f(B) <= f(A) 成立 |
上述的协变和逆变出现的情况,就相当于对两个类继承的类所表示的范围进行操作,这里以一个 Super
父类 和 子类 Sub
来说明
- 之所以出现定义中的规律,正是因为类型之间的关系,
Object
相当于总范围
协变参数类型
方法参数的传入类型可以为参数类型以及它的导出类型(子类)
1 | // test |
协变返回类型
导出类(子类) 重写父类方法的 返回类型 可以是父类方法的 返回类型的导出类型(子类)
1 | // test |
泛型不支持协变
泛型是不支持协变的,实现协变需要使用 泛型的通配符
1 | // test |
- 数组支持协变
- 泛型不支持协变
泛型边界
在泛型声明的位置使用
extends
关键字可以指定泛型参数T
的上界, 只能有一个类,可以有多个接口作为上界,放在泛型声明的后面,使用&
连接
泛型边界的意义
由于泛型的擦除,
<T>
泛型会被 擦除到他们的第一个边界,没有设置边界的泛型参数在运行时,会被 擦除到Object
1 | // test |
- 泛型边界的声明使用的是
&
与运算符,所以需要同时满足继承类并实现所有的接口
通配符
需要注意的地方就是区分 通配符 和 泛型参数,也是我半天没弄明白的地方
- 泛型参数。用于声明泛型参数,泛型的边界也在声明中加入
- 通配符。其实就是相当于确切类型的占位符,放置在使用泛型的地方
? 非限定通配符
<?>
通配符,其实就是<? extends Object>
的显示扩展,但是在使用的时候又稍微有点不一样
1 | // test |
- 只有在
<? extends Object>
接收 原生类型 的时候,才会出现编译警告
使用了 <?>
统配符的引用类型
对于像
void set(T t)
这种泛型操作都会时效,因为<?>
类似于<? extends Object>
不能确定持有的泛型是哪一种,因此砍掉了所有的泛型入口对于
T get()
泛型出口,统一的接收类型就又变成了Object
限定通配符
限定通配符包括两种
? extends Type
上界限定通配符? super Type
下界限定通配符
1 | // test |
两种限定通配符分析
? extends Type
作为泛型类型的时候,插入操作都会失效,获取操作正常 接收类型为UpperBound
插入: 对于
? extends Type
这一个通配符而言而言,并不能知道确切类型,所以所有关于泛型的插入一刀切获取: 能够统配的类型肯定为
Type
的导出类型(子类型)1
UpperBound u = list.get(index);
? super Type
作为泛型类型的时候,插入extends LowerBound
的对象正常,获取的接收类型为实际类型插入: 编译器可以知道统配的类型是
LowerBound
的基类型(父类型),所以LowerBound
以及他的导出类型(子类型) 肯定可以插入获取: 对于
? super LowerBound
只知道为LowerBound
的一个基类型,对于? super LowerBound
的接受类型就为Object
(根类型)1
2list = new ArrayList<Type>();
Type t = list.get(index);
区别对比
限定通配符 | 泛型返回类型的接收类型( T get() ) |
泛型参数/插入类型( void set(T t) ) |
---|---|---|
? extends UpperBound |
UpperBound |
x |
? super LowerBound |
Object |
LowerBound 以及其子类型 |
泛型捕获转化
编译器可以通过原生类型推断出实际的泛型参数
1 | // test |
- 如果只将原生类型传入到有拥有泛型参数的方法中,就会抛出未检查的警告,而使用
<?>
来接收原生类型,可以推断出其内部的泛型类型 <T>
接收<?>
不会发出警告
自限定泛型
将泛型指定为
<A extends SelfBound<A>>
自限定泛型,只有类A
满足了class A extends Bound<A>
这种声明方式,才能作为该类的泛型参数自限定的作用就是使基类中的类型,随着子类的指定而发生变化,将父类泛型方法的返回类型指定为了
DerivedGS
这个导出类型基类为一个简单的泛型类
class Basic<E>
子类实现自限定
class Derived extends Basic<Derived> {}
1 | // test |
DerivedSetter
子类和OrdinarySetter
父类的方法的 参数类型 不一致,为两个不同的方法,属于 方法重载DerivedGS
中通过super.set(sub)
调用GenericSetter # set
方法成功,说明它重写了父类方法,DerivedGS # set
覆盖了父类的方法,属于 方法重写
潜在类型机制的补偿
对于存在相同名称方法的类,他们之间没有继承关系,可以通过 反射 来完成对其的泛化使用
1 | // test |
- 使用
Class # getMethod
只能获取到类中public
权限的方法,Class # getDeclaredFields
可以获取到私有权限的属性