Java 泛型
泛型的本质是参数化类型。简单解释就是,将参数的数据类型也当作参数对待。泛型的目的就是为了写一套代码,可以到处通用,不用担心类型安全的问题。泛型可以用在类、接口、方法中对应的就是泛型类、泛型接口和泛型方法。
一、为什么要引入泛型?
我们先看一个例子:
获取一个字符串对象,打印如下:
引入 Object 类型
这个类只能适用字符串类型,那要获取整型等其他数据类型怎么办呢?于是我们想到了 Object 类型,它可以表示所有 Java 类类型数据,于是改造下:
打印结果,字符串类型和上面的打印结果是相同的:
让方法返回一个 Object 类型数据,这样似乎就解决了问题。但还是有个问题,因为方法返回的是 Object 类,返回值参与其它类型数据的计算时需要强制类型转换。如:
使用泛型
Object 类型数据使用时需要事先知道参与计算的具体类型,有时候我们很难判断需要什么类型,如果写错了,那就会报错。为了少写代码,且不出错,于是就引入了泛型。通过泛型改造上面的类:
打印结果:
使用泛型后就不用强制转换数据类型,代码也变得通用了。
泛型的定义
使用泛型的方式有3种,分别是:
泛型类泛型方法泛型接口上面的代码中用的是 T 来代指数据类型,也就是泛型参数。T 不是唯一的,可以用任意大写字母替代,不过一般遵循如下原则:T:指一般的任何类。E:元素 Element 的意思,或者 Exception 异常的意思。K:键,Key 的意思。V:值,Value 的意思,通常与 K 一起配合使用。
泛型类,定义一个泛型类:
在类名后面需要用<>包裹,T 就是类型参数,多个类型参数,用不同的大写字母且“,”分隔。创建泛型类:
<>中传入对应的类型,类中的 T 就会被对应的类型取代。
泛型方法,定义一个泛型方法:
泛型方法的定义是在返回类型前面加,返回类型也可以用泛型代指,如 T。方法中的类型化参数 T 不用<>。没有返回类型的泛型方法:
泛型类和泛型方法可以同时使用,如:
泛型方法和普通方法的参数没有关联。
泛型接口
定义泛型接口:
泛型接口和泛型类的定义类似。
泛型通配符?
有时我们会碰到这样的情况,一个夫类型的集合需要兼容子类,或者子类集合要兼容父类。如:
Integer继承自Number 类,但不代表 List继承自 List,泛型也没有继承关系,所以不能像变量类型一样兼容使用。但有时会又这类需求,于是引入?通配符:
extends T>上限通配符,指类型必须是 T 的子类,或 T 类型。
super T>下限通配符,指类型必须是 T 的父类,或 T 类型。 如:
还有一种是无限通配符:>本质是 extends Object>,表示不知道什么类型。
无限通配符可以方便使用但也是有限制的,无限通配符意味着不知道具体类型,所以数据只能读取,不能写入。如:
类型擦除
泛型只在编译阶段有效,编译后类型会被擦除。如:
打印结果为 true,也即 l1 和 l2 的类型相同,都是java.util.ArrayList,泛型信息被擦除了。
泛型的限制
泛型不支持 8 中基本数据类型,需要用对应类型的包装类。
List l = new ArrayList<>(); // 编译报错
创建泛型类数组时只能创建无限定通配符的。
因为类型擦除后就不知道数组中具体存储的哪种类型数据了。
泛型与反射
先看个例子:
创建一个接受Integer类型的泛型数组 list,如果往 list 中插入String类型数据时编译出错,类型限制。看下 List 的 add 方法:
E代表任意类型,所以类型擦除后 add 方法就变成了如下方式:
boolean add(Object obj);
通过反射可以获取对象实例,并访问对象数据。如:
因为泛型的类型擦除性质,利用反射就可以绕过编译器的类型检查,这也说明了反射会引入安全问题,需要谨慎使用。