0%

泛型

什么是泛型?

泛型是编译器的类型安全检查机制,让更多类型错误暴露在编译期,提高程序稳定性。

最常见的应用场景是集合

List list = ArrayList();
list.add("a");
list.add(1);

如果希望list只接受字符串类型,使用泛型很容易做到这一点

List<String> strList = ArrayList<>();
strList.add("a");
strList.add(1);//编译失败,只能是String类型

泛型类型

泛型类型是从类型上参数化的类或接口.

泛型类型定义

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<>();
list.add(new Integer(1));
list.add(new Double(1));

但是请注意下面代码

public void setList(List<Number> list){}
setList(new ArrayList<Integer>());//compile error

对于fun方法参数如果传递ArrayList会编译报错,虽然Integer是Number的子类型,但是ArrayList并不是List的子类型,要解决这个问题可以使用下面讲到的通配符来解决

public void setList(List<? extends Number> list){}
setList(new ArrayList<Integer>())

通配符

在泛型代码中使用问号(?)表示未知类型,泛型类型调用时作为类型实参。

// ArrayList的addAll方法使用通配符
public boolean addAll(Collection<? extends E> c) {}

注意:因为通配符作为泛型类型调用的实参,所以不能用于泛型类或接口定义(泛型类定义中的类型变量是形参)

上界通配符

上界通配符? extends限定上边界,限定类型是指定类型或它的子类型

比如下面方法参数是一个集合既想接收Integer类型又想接收Double类型可以使用上界通配符

public void addNumbers(List<? extends Number> list){}
addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Double>());

注意以下代码

List<? extends Number> numbers = new ArrayList<>();
numbers.add(new Integer(1));//compile error
numbers.add(new Double(1));// compile error

使用上界通配符在对集合进行写操作时编译报错,这是因为虽然虽然限定了类型为Number及子类,但是实际类型可能是Integer也可能是Double编译器无法判断唯一性,所以不允许进行写操作。

无界通配符

无界通配符使用?表示未知类型来指定泛型类型实参

无界通配符应用场景:

① 使用Object类提供的功能实现的方法

② 泛型代码不依赖于泛型形参

​ 例如Class<?>,因为Class中的大多数方法不依赖于T

例如以下方法

public void println(List<Object> list){
for(Object item : list){
System.out.println(item);
}
}

如果想要打印List或List是不可以的,这时候可以使用无界通配符打印所有类型的List

public void println(List<?> list){
for(Object item:list){
System.out.println(item);
}
}
下界通配符

使用? super来限定下边界,限定类型为指定类型或它的父类型

例如以下方法

public void addNumbers(List<? super Integer> list){}
addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Number>());

使用? super Integer限定接收的类型为Integer或它的父类型

注意以下代码

List<? super Integer> list = new ArrayList<>();
Number num = 1;
list.add(new Integer(1));
list.add(num);//compile error
list.add(new Object());//compile error

向限定下界为Integer的list中添加Number类型时编译报错,因为虽然限定了下界为Integer或它的父类,但是这个类型可能是Number或者Object编译器无法判断,而类型又必须唯一,为了类型安全只能写入Integer或它的子类型。

如果想要既可以写入Integer又可以写入Number,限定下边界需要改为Number

List<? super Number> list = new ArrayList<>();
Number num = 1;
list.add(new Integer(1));
list.add(num);

在来看以下代码

List<? super Number> list = new ArrayList<>();
list.add(new Integer(1));
Number item = list.get(0);//compile error
Object item = list.get(0);//compile success

使用下界通配符后list集合获取元素只能用Object接收,这是因为元素了类型可能是符合限定条件的所有类型,可能是Integer也可能是Double,编译器无法确定所以不能用Number来接收。相当于失去了读的属性

可以看出限定上边界以后泛型类型相当于只有读权限,限定下边界相当于只写权限,关于通配符使用原则可以看下面通配符使用指南。

通配符使用指南

我们在通配符使用过程中可能会有困惑,什么时候使用上界通配符,什么时候使用下界通配符,官方建议是根据”in”和”out”原则,也有人叫作生产消费者原则

例如

copy(src,dest);//src变量作为"in"变量,dest作为"out"变量

“in”变量使用上界通配符,使用extends关键字(生产者)

“out”变量使用下界通配符,使用super关键字(消费者)

可以使用Object的方法来访问”in”变量使用无界通配符

变量既要读数据又要写数据时不要使用通配符

类型擦除

泛型类型是jdk1.5引入的,为了与以前版本兼容,编译器在编译阶段会将泛型类型擦除,同时对泛型类型做类型转换。

对于无界类型形参替换为Object

对于有界类型形参替换为边界类型

类型擦除示例

// 无界类型
// 擦除前
Class Box<T>{
private T t;
public void setValue(T t){
this.t = t;
}
public T getValue(){
return t;
}
}
// 擦除后
Class Box {
private Object t;
public void setValue(Object t){
this.t = t;
}
public void getValue(){
return t;
}
}
// 有界类型
// 擦除前
Class Box<T extends Integer>{
private T t;
public void setValue(T t){
this.t = t;
}
public T getValue(){
return t;
}
}
// 擦除后
Class Box{
private Integer t;
public void setValue(Integer t){
this.t = t;
}
public Integer getValue(){
return t;
}
}

来看一到面试题

List<String> list1 = new ArrayList<>():
List<Integer> list2 = new ArrayList<>():
System.out.print(list1.getClass()==list2.getClass());

最终输出结果是true,因为两个list在类型擦除后Class类型都是List.class

泛型的限制

无法用基本类型实例化泛型类型
List<int> list = new ArrayList<int>();//compile error
List<Integer> list = new ArrayList<Integer>();//compile success
无法创建类型形参的实例
public static <E> append(List<E> list){
E e = new E();//compile error
list.add(e);
}

// 解决办法是提供Class通过反射创建
public static <E>append(List<E> list,Class<E> cls){
E e = cls.newInstance();
list.add(e);
}
无法声明为类型形参的静态字段
public class Box<T>{
private static T t;//compile error
}

因为静态字段是对象共享的如果允许这样操作,每个对象可以在创建实例时可以指定不同类型,但是它不能同时是多种类型

不能使用参数化类型进行类型转化或instanceof
public void check(List<E> list){
if(list instanceof ArrayList<Integer>)//compile error
}

因为泛型类型会被擦除

无法重载类型参数擦除后原始类型相同的方法
public void set(List<String> strList);
public void set(List<Integer> intList);

类型擦除后原始类型都为Object因此无法重载

无法创建参数化类型的数组
无法创建,捕获或抛出参数化类型的对象

参考

https://docs.oracle.com/javase/tutorial/java/generics/index.html

https://pingfangx.github.io/java-tutorials/java/generics/types.html

https://blog.csdn.net/briblue/article/details/76736356