什么是泛型?
泛型是编译器的类型安全检查机制,让更多类型错误暴露在编译期,提高程序稳定性。
最常见的应用场景是集合
List list = ArrayList(); |
如果希望list只接受字符串类型,使用泛型很容易做到这一点
List<String> strList = ArrayList<>(); |
泛型类型
泛型类型是从类型上参数化的类或接口.
泛型类型定义
class name<T1,T2,…Tn>{}
举例:
class Box<T>{} |
名词解释:
类型形参:T就是泛型类Box的类型形参
类型实参:泛型调用时替换T的值,例如Box
strBox ;String就是类型实参
调用和实例化
泛型类型调用
要在代码中引用泛型类型要使用泛型类型调用,将泛型形参T替换为具体的值
例如上面声明的泛型类Box的类型调用如下:
Box<String> strBox; |
泛型类型调用也叫作参数化类型parameterized type
实例化
泛型类型实例化与普通类类似,使用new关键字,在类名和括号之间使用尖括号标明泛型类型
Box<String> strBox = new Box<>();// 由于类型推断可以省略<>里面的泛型类型 |
泛型方法
泛型方法是指在声明方法时引入泛型类型
例如:
public <T>void setValue(T t){} |
在返回值前声明泛型类型,返回或参数使用泛型类型
有界类型形参
有时我们希望对泛型类型形参边界进行限定,使用extends关键字限定上边界
public Box<T extends Number>{} |
如果我们希望Box泛型类型实参既可以是Integer又可以是Double可以使用上面方法进行限定
注意:泛型类型边界只能限定上边界,无限定下边界方法。不可与通配符的限定下边界混淆
泛型类型继承
泛型类型实参也存在继承关系,只要类型兼容可以将一个类型分配给另一种类型
List<Number> list = new ArrayList<>(); |
但是请注意下面代码
public void setList(List<Number> list){} |
对于fun方法参数如果传递ArrayList
public void setList(List<? extends Number> list){} |
通配符
在泛型代码中使用问号(?)表示未知类型,泛型类型调用时作为类型实参。
// ArrayList的addAll方法使用通配符 |
注意:因为通配符作为泛型类型调用的实参,所以不能用于泛型类或接口定义(泛型类定义中的类型变量是形参)
上界通配符
上界通配符? extends限定上边界,限定类型是指定类型或它的子类型
比如下面方法参数是一个集合既想接收Integer类型又想接收Double类型可以使用上界通配符
public void addNumbers(List<? extends Number> list){} |
注意以下代码
List<? extends Number> numbers = new ArrayList<>(); |
使用上界通配符在对集合进行写操作时编译报错,这是因为虽然虽然限定了类型为Number及子类,但是实际类型可能是Integer也可能是Double编译器无法判断唯一性,所以不允许进行写操作。
无界通配符
无界通配符使用?表示未知类型来指定泛型类型实参
无界通配符应用场景:
① 使用Object类提供的功能实现的方法
② 泛型代码不依赖于泛型形参
例如Class<?>,因为Class
中的大多数方法不依赖于T
例如以下方法
public void println(List<Object> list){ |
如果想要打印List
public void println(List<?> list){ |
下界通配符
使用? super来限定下边界,限定类型为指定类型或它的父类型
例如以下方法
public void addNumbers(List<? super Integer> list){} |
使用? super Integer限定接收的类型为Integer或它的父类型
注意以下代码
List<? super Integer> list = new ArrayList<>(); |
向限定下界为Integer的list中添加Number类型时编译报错,因为虽然限定了下界为Integer或它的父类,但是这个类型可能是Number或者Object编译器无法判断,而类型又必须唯一,为了类型安全只能写入Integer或它的子类型。
如果想要既可以写入Integer又可以写入Number,限定下边界需要改为Number
List<? super Number> list = new ArrayList<>(); |
在来看以下代码
List<? super Number> list = new ArrayList<>(); |
使用下界通配符后list集合获取元素只能用Object接收,这是因为元素了类型可能是符合限定条件的所有类型,可能是Integer也可能是Double,编译器无法确定所以不能用Number来接收。相当于失去了读的属性
可以看出限定上边界以后泛型类型相当于只有读权限,限定下边界相当于只写权限,关于通配符使用原则可以看下面通配符使用指南。
通配符使用指南
我们在通配符使用过程中可能会有困惑,什么时候使用上界通配符,什么时候使用下界通配符,官方建议是根据”in”和”out”原则,也有人叫作生产消费者原则
例如
copy(src,dest);//src变量作为"in"变量,dest作为"out"变量 |
“in”变量使用上界通配符,使用extends关键字(生产者)
“out”变量使用下界通配符,使用super关键字(消费者)
可以使用Object的方法来访问”in”变量使用无界通配符
变量既要读数据又要写数据时不要使用通配符
类型擦除
泛型类型是jdk1.5引入的,为了与以前版本兼容,编译器在编译阶段会将泛型类型擦除,同时对泛型类型做类型转换。
对于无界类型形参替换为Object
对于有界类型形参替换为边界类型
类型擦除示例
// 无界类型 |
// 有界类型 |
来看一到面试题
List<String> list1 = new ArrayList<>(): |
最终输出结果是true,因为两个list在类型擦除后Class类型都是List.class
泛型的限制
无法用基本类型实例化泛型类型
List<int> list = new ArrayList<int>();//compile error |
无法创建类型形参的实例
public static <E> append(List<E> list){ |
无法声明为类型形参的静态字段
public class Box<T>{ |
因为静态字段是对象共享的如果允许这样操作,每个对象可以在创建实例时可以指定不同类型,但是它不能同时是多种类型
不能使用参数化类型进行类型转化或instanceof
public void check(List<E> list){ |
因为泛型类型会被擦除
无法重载类型参数擦除后原始类型相同的方法
public void set(List<String> strList); |
类型擦除后原始类型都为Object因此无法重载
无法创建参数化类型的数组
无法创建,捕获或抛出参数化类型的对象
参考
https://docs.oracle.com/javase/tutorial/java/generics/index.html
https://pingfangx.github.io/java-tutorials/java/generics/types.html