A:今天来聊聊一些基础吧,简单说说泛型。
B:泛型不是一直在用嘛,很简单的啊,一对 <> 就能指定泛型,然后使用。
A:那你先讲讲什么叫泛型吧?
B:Emmm...(一时语塞,虽然一直在用,但是一时语塞,组织不起来语言)。
A:概念不清,意识模糊,只是停留在会用阶段,却不知道本质。
B:(假装一时忘记了而已)
A:既然经常使用泛型,那么应该知道泛型擦除机制吧。
B:这个我知道的,很简单嘛,为了兼容 JDK5 以前的版本。
A:既然在编译时,会有泛型擦除机制,那么在编译打包后,例如:Gson 又是怎么能正确解析成指定的泛型类呢?按照前面所说,泛型擦除了啊。
B:Emmm... 彻底蒙圈
泛型,作为一枚野生 Android 搬砖工,最常用的莫过于 泛型,在所有的架构中,泛型 是核心,所以说:泛型 是通往高阶的基础。哪怕你调用第三方框架再熟练,也只是个熟练的 API 调用者而已,出个问题就无法解决,只会甩锅给三方库,但是问题还是得不到解决。所以,在成长的路上,首先要彻底了解并掌握 泛型,只有这样,才能不至于在后期举步维艰。
1、先了解一下泛型是什么
先看一下 维基百科 的定义:
泛型的定义主要有以下两种:
- 在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表類,不能代表个别对象。(这是当今常见的定义)
- 在程序编码中一些包含参数的类。其参数可以代表类或对象等等。(现在人们大多把这称作模板)
在下定义之前,再了解一下参数分类,一般情况下,参数分为以下两类:
- 形式参数:输入是值;
- 类型参数:输入是类型;
接下来,结合代码进行简单介绍分析(已经了解的可跳过)。
1.1 形式参数
在工作中,经常编写的就是方法,例如:
// 其中,a、b 都是形式参数,传入的是已经构建好的对象(Java 一切皆对象)
public int add (int a, int b) {
return a + b;
}
根据方法本质来说,需要使用到传入的对象的某个值来进行相应的加工处理,这就是形式参数,简称 形参;
1.2 类型参数
处理需要值以外,有时候还需要指定类型,例如:
List<String> list = new ArrayList<>();
在 <> 中,有一个类型 String,注意,是 类型 哟,而不是对象,至于原因吗,好好想想...再想想,这个 String 你实例化了吗,现在能理解 对象 和 类型 的区别了吧,<String> 的目的就是为了告诉编译器,在这个 List 中,指定的是 String 类型的,只能对 String 类型的对象进行处理,不信的话你可以调用 list.add(0) 试试,看看会不会报错,至于原因,后续会讲。
由上可知,泛型是 类型化参数。
2、为什么要使用泛型
- 在编译期进行更强的类型检查;
- 消除类型转换;
- 能够实现通用算法;
接下来,逐步进行解释:
2.1 在编译期进行更强的类型检查
Talk is cheap, show me the code. 先上一段毒代码:
// 一个用于存储 int 数据的集合
List list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add("4"); // 失误操作
for (int i = 0; i < list.size(); i++) {
int num = (int) list.get(3);
System.out.println("num is " + num);
}
由于没有类型约束,编译器不会报错,但是,在运行时,程序会直接崩溃,并报 ClassCastException,这要是发生在线上,就是生产事故了。那么如何在编译器就能检测出这个错误呢,这就该 泛型 登场了,修改代码如下:
// 一个用于存储 int 数据的集合
List list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add("4"); // 此次会直接报错,编译无法通过
int num4 = ist.get(3); // 同时,这里没有强转了
在约定类型后, list 只能添加对应的类型,也就是示例代码中的 Integer,如果传入 String,是无法通过编译的,这样就将错误由 运行时错误 提前为 编译时错误,并且在多人合作时,也不用担心其他小伙伴添加错误的类型对象。
2.2 消除类型转换
List list = new ArrayList<>();
list.add(1);
int num = (int) list.get(3); //由于 List 未指定类型,系统无法得知 get 后的类型,因此,需要进行强转
List list = new ArrayList<Integer>();
list.add(1);
int num = list.get(0); // 由于指定了类型 `Integer`,系统会推断出对应的类型,可以省去强转,也可避免强转成错误的类型,提高了代码的健壮性
2.3 能够实现通用算法
没有泛型以前的 add 世界:
public double add (int a, int b) {
return a + b;
}
public double add (float a, float b) {
return a + b;
}
public double add (double a, double b) {
return a + b;
}
public double add (long a, long b) {
return a + b;
}
public double add (byte a, byte b) {
return a + b;
}
public double add (short a, short b) {
return a + b;
}
// 如果根据排列组合来写,方法数量只会呈指数级增长
居然要写 6 个方法,才能实现兼容,使用泛型之后的世界:
public <N extends Number> double add(N a, N b) {
return a.doubleValue() + b.doubleValue();
}
由于使用了泛型,方法数量减少为 1 个,实现了代码复用。因此,掌握好泛型,是通往高阶之路的基础。
3、泛型分类
- 泛型类
- 泛型接口
- 泛型方法
// 泛型类
public class Box<T> {
public T t;
}
// 泛型接口
pubclic interface Box<T>{
}
// 泛型方法
public <T> void test(T t) {
}
4、常用的类型参数名称
- E --> Element
- K --> Key
- N --> Number
- T --> Type
- V --> Value
- S、U、V etc --> 2nd、3rd、4th types
只是一些共识,代表指定什么意思,跟着用就对了,如果你想特立独行,那...也不是不可以。
5、泛型扩展
在很多需求中,泛型 <> 中的类型可能不是一个确定的类型,那么能不能不指定具体类型,比如某一个类的父类,以便后续扩展呢接下来,答案是肯定的,接下来,将介绍一下 受限通配符 和 非受限通配符,从而让泛型也具有可扩展性。
5.1 继承
5.1.1 接口的继承
interface GenericInterface<T>
/**
* 接口继承方式一
*/
interface GenericChildInterface1<T> : GenericInterface<T>
/**
* 接口继承方式二
*/
interface GenericChildInterface2 : GenericInterface<String>
比较简单,就不过多介绍了。
5.1.2 类的继承
open class GenericClass<T>
/**
* 泛型继承方式一
*/
class GenericChildClass1<T> : GenericClass<T>()
/**
* 泛型继承方式二
*/
class GenericChildClass2 : GenericClass<String>()
也比较简单,看一眼即可。
5.1.3 多继承
open class A
open class B
interface C
interface D
/**
* 错误写法
* 原因:注意单继承,这里 A、B 都是 class,同时继承多个 class,违背单继承原则,无法编译通过
*/
class MultipleGenericClass1<T : A, B, C, D>
/**
* 错误写法
* 原因:注意继承规则,如果存在 class,那么 class 一定要排在前面
*/
class MultipleGenericClass2<T : C, A, D>
/**
* 正确写法
*/
class MultipleGenericClass3<T : A, C, D>
看注释即可。
5.2 通配符
5.2.1 受上限控制的通配符
语法格式:<? extends XXX>
优点:扩大兼容的范围。
List<Number> list1; // list1 只能匹配 Number 类型的数据
List<? exntends Number> list2; // list2 可以匹配 Number 类型的数据及其子类
5.2.2 受下限控制的通配符
语法格式:<? super XXX>
优点:扩大兼容的范围;可以建立泛型类、接口间的联系。
List<Integer> list1; // list1 只能匹配 Integer 类型的数据
List<? super Integer> list2; // list2 可以匹配 Integer 类型的数据及其祖类/接口
5.2.3 非受限的通配符
语法格式:<? >
关键使用场合:
- 一个方法,而这方法的实现可以利用 Object 类中提供的功能时泛型类中的方法不依赖类型参数时如 List.size() 方法,它并不关心 List 中元素的具体类型;
- List<XXX> 是 List<?> 的一个子类型理解 List<Object> 和 List<?> 的不同:差在 NULL 处理,前者不支持,而后者却可接受一个 null 入表;
泛型类/接口之间,没有任何关联,例如 List<Integer> 和 List<Number> 没有任何关联,不过可以使用通配符来建立联系。
通配符只能在泛型方法中使用,泛型类、泛型接口无法使用。
通配符只能在泛型方法中使用,泛型类、泛型接口无法使用。
通配符只能在泛型方法中使用,泛型类、泛型接口无法使用。
重要的情况说 3 遍。
6、PESC 原则
PESC 原则的好处:提升了 API 的灵活性。
6.1 PE(Producer extends)
如果你只需要从集合中获得类型 T , 使用 <? extends T> 通配符。
public void test(List<? extends Number> list) {
list.add(0); // 错误, 此处 list 作为一个生产者,后续操作只能消费,不能继续生产
}
6.2 SC(Consumer super)
如果你只需要将类型T放到集合中, 使用 <? super T> 通配符。
public void test(List<? super Number> list) {
list.add(0); // 错误, 此处 list 作为一个消费者,后续操作只能生产,不能继续消费
}
当然,如果你既想当 生产者,又想当 消费者,那么不使用通配符即可。
7、类型擦除
在讲 类型擦除 之前,先来了解一下 泛型 历史。泛型是在 JDK5 才出现的,为了保证旧的 JDK 版本能正常运行,那么只能对 泛型 操刀了,将 泛型 处理成低版本能识别的信息,由此可见,Java/Kotlin 中的 泛型 为 伪泛型。
那么,如何兼容呢?类型擦除 就是在该场景下诞生的。
擦除规则:
编译器会把泛型类型中所有的类型参数替换为它们的上(下)限,如果没有对类型参数做出限制,那么就替换为 Object 类型。(因此,编译出的字节码仅仅包含了常规类,接口和方法。)
在必要时插入类型转换以保持类型安全。
生成桥方法以在扩展泛型时保持多态性。
字节码如下,有兴趣的话可以研究研究:
// 在子类,泛型设置为 Integer,那么 setT 的参数就变为 Integer, 但是基类却不知道这个情况
public getT()Ljava/lang/Integer;
@Lorg/jetbrains/annotations/Nullable;() // invisible
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL com/wd/kt/generic/Box.getT ()Ljava/lang/Object;
CHECKCAST java/lang/Integer
ARETURN
L1
LOCALVARIABLE this Lcom/wd/kt/generic/IntegerBox; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// 由于父类不知道子类的情况,为了不出现父类编译错误,因此有 bridge 方法,将子类与父类类型关联起来
// access flags 0x1041
public synthetic bridge getT()Ljava/lang/Object;
L0
LINENUMBER 3 L0
ALOAD 0
INVOKEVIRTUAL com/wd/kt/generic/IntegerBox.getT ()Ljava/lang/Integer;
ARETURN
MAXSTACK = 1
MAXLOCALS = 1
8、泛型中的约束和局限性
8.1 不能实例化类型变量
/**
* 错误
* 原因:泛型中不能实例化类型变量
*/
private var t:T = T()
8.2 静态域中不能引用类型变量
/**
* 错误
* 原因:静态域中不能引用类型变量
*/
private var t = T()
8.3 静态方法中不能引用类型变量
/**
* 错误
* 原因:静态方法中不能引用类型变量
*/
fun test(t: T) {
}
/**
* 静态方法如果是泛型方法,那么可以正常使用
*/
fun <T> test(t: T) {
}
8.4 基础类型不能作为泛型实例化类型,包装类型可以
val list = arrayListOf<double>() // 错误写法,基础类型不能作为泛型实例化类型
val list = arrayListOf<Double>() // 正确写法,包装类型可以作为泛型实例化类型
8.5 泛型不能使用 instanceof 的
语法层面规定的,究其原因就是泛型擦除机制,在编译器泛型就不存在了,所以 instanceof 无效。
8.6 两个泛型类相同
val boxInt = Box<Int>();
val boxString = Box<String>();
System.out.println(boxInt.class == boxString.class); // true
因为泛型擦除机制,在泛型被擦除后,boxInt 与 boxString 的类型是相同的,都是 Box 类。
8.7 泛型可以声明数组,但是不能初始化泛型数组
Box<Double>[] boxs; // 没有异常
boxs = new Box<Double>[10]; // 错误,泛型数组不能初始化
8.8 泛型不能 extends Exception/Throwable*/
public <T extends Throwable> void test(T t) {
try {
} catch (T t) { // 错误,不能捕捉异常对象
}
}
// 编写可以捕获异常代码
public <T extends Throwable> void test(T t) throws T {
try {
} catch (Throwable e) {
throw t;
}
}
8.9 Java 中 Arrays 不能使用泛型
/**
* 错误示例
* 在 Java 中,数组不支持泛型
*/
Arrays arrays = new Arrays<String>();
8.10 Java 中 List 与 List <?> 区别在哪
List:原始类型,不是泛型,所以不涉及到类型检查;
List <?>: 泛型,涉及到类型检查;
8.11 Java 中 List<Object> 与 List<?> 的区别在哪
List<?>:是一个未知类型的 List,可以把 List<String>、List<Integer> 等赋值给 List<?>;
List<Object>:是任意类型的 List,却不能把 List<String>、List<Integer> 等赋值给 List<Object>;
9、泛型区分
先定义一个泛型类。
public class TestBox<T> {
public T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
public <T> void test() {
// 原始类型
TestBox box1 = new TestBox();
// signature Lcom/wd/generic/TestBox<Ljava/lang/Object;>;
// declaration: box2 extends com.wd.generic.TestBox<java.lang.Object>
TestBox<Object> box2 = new TestBox<>();
// signature Lcom/wd/generic/TestBox<*>;
// declaration: box3 extends com.wd.generic.TestBox<?>
TestBox<?> box3 = new TestBox<>();
// signature Lcom/wd/generic/TestBox<TT;>;
// declaration: box4 extends com.wd.generic.TestBox<T>
TestBox<T> box4 = new TestBox<>();
// signature Lcom/wd/generic/TestBox<+TT;>;
// declaration: box5 extends com.wd.generic.TestBox<? extends T>
TestBox<? extends T> box5 = new TestBox<>();
// signature Lcom/wd/generic/TestBox<-TT;>;
// declaration: box6 extends com.wd.generic.TestBox<? super T>
TestBox<? super T> box6 = new TestBox<>();
}
10、泛型与反射
前面我们讲到了泛型擦除,那么我们常用的 Gson、FastJson 等框架,又怎么能正确转换成我们需要的泛型呢?
其实,泛型虽然擦除了,从字节码我们可以看出还是保留了关键信息:
signature: 指定了泛型对应的类型;
declaration:声明了原始代码信息;
既然保留的有信息,那么在使用时,通过特殊手段获取到该信息,那么就能继续正常使用泛型了,加下来结合代码看看是如何实现的。
class Test {
val map = HashMap<String, Number>()
}
fun main() {
val field = Test::class.java.getDeclaredField("map") // 获取 field
val paramType = field.genericType as ParameterizedType // 获取 ParameterizedType,ParameterizedType 中存储的有泛型信息
for (item in paramType.actualTypeArguments) { // 获取实际泛型信息,并打印
println(item.typeName)
// 打印结果为:
// java.lang.String
// java.lang.Number
}
}
由此可见,虽然泛型擦除了,但是并不妨碍运行期获取泛型实际类型信息,方法总是有的,研究一下或许就会有收获。
最后,示例项目地址信息 示例代码,欢迎交流学习,如果错误,请指正。